From 8a1c6b596b417cd636f04e33c817e0435f029cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Sun, 13 Sep 2026 18:49:11 +0200 Subject: [PATCH] feat(mobile): add session goal section and /goal composer support https://github.com/Kilo-Org/cloud/pull/6117 --- .../chat-composer-slash-commands-goal.test.ts | 186 ++++++++++++++++++ .../chat-composer-slash-commands.test.ts | 6 + .../agents/chat-composer-slash-commands.ts | 43 ++++ .../components/agents/chat-composer.test.ts | 94 ++++++++- .../src/components/agents/chat-composer.tsx | 41 ++++ .../agents/mobile-session-manager.ts | 10 +- .../mobile-session-page-adapter.test.ts | 34 ++++ .../agents/mobile-session-page-adapter.ts | 14 +- .../agents/session-detail-content.test.ts | 42 +++- .../agents/session-detail-content.tsx | 171 +++++++++++++++- .../agents/session-detail-queue.test.ts | 4 + .../agents/session-goal-actions.test.ts | 91 +++++++++ .../components/agents/session-goal-actions.ts | 68 +++++++ .../agents/session-goal-section.tsx | 59 ++++++ .../agents/use-interaction-handlers.ts | 18 +- apps/mobile/src/i18n/locales/af.json | 16 ++ apps/mobile/src/i18n/locales/am.json | 16 ++ apps/mobile/src/i18n/locales/ar.json | 16 ++ apps/mobile/src/i18n/locales/az.json | 16 ++ apps/mobile/src/i18n/locales/be.json | 16 ++ apps/mobile/src/i18n/locales/bg.json | 16 ++ apps/mobile/src/i18n/locales/bn.json | 16 ++ apps/mobile/src/i18n/locales/bs.json | 16 ++ apps/mobile/src/i18n/locales/ca.json | 16 ++ apps/mobile/src/i18n/locales/ckb.json | 16 ++ apps/mobile/src/i18n/locales/cs.json | 16 ++ apps/mobile/src/i18n/locales/cy.json | 16 ++ apps/mobile/src/i18n/locales/da.json | 16 ++ apps/mobile/src/i18n/locales/de.json | 16 ++ apps/mobile/src/i18n/locales/el.json | 16 ++ apps/mobile/src/i18n/locales/en.json | 16 ++ apps/mobile/src/i18n/locales/es.json | 16 ++ apps/mobile/src/i18n/locales/et.json | 16 ++ apps/mobile/src/i18n/locales/eu.json | 16 ++ apps/mobile/src/i18n/locales/fa.json | 16 ++ apps/mobile/src/i18n/locales/fi.json | 16 ++ apps/mobile/src/i18n/locales/fil.json | 16 ++ apps/mobile/src/i18n/locales/fr.json | 16 ++ apps/mobile/src/i18n/locales/ga.json | 16 ++ apps/mobile/src/i18n/locales/gl.json | 16 ++ apps/mobile/src/i18n/locales/gu.json | 16 ++ apps/mobile/src/i18n/locales/ha.json | 16 ++ apps/mobile/src/i18n/locales/he.json | 16 ++ apps/mobile/src/i18n/locales/hi.json | 16 ++ apps/mobile/src/i18n/locales/hr.json | 16 ++ apps/mobile/src/i18n/locales/ht.json | 16 ++ apps/mobile/src/i18n/locales/hu.json | 16 ++ apps/mobile/src/i18n/locales/hy.json | 16 ++ apps/mobile/src/i18n/locales/id.json | 16 ++ apps/mobile/src/i18n/locales/ig.json | 16 ++ apps/mobile/src/i18n/locales/is.json | 16 ++ apps/mobile/src/i18n/locales/it.json | 16 ++ apps/mobile/src/i18n/locales/ja.json | 16 ++ apps/mobile/src/i18n/locales/ka.json | 16 ++ apps/mobile/src/i18n/locales/kk.json | 16 ++ apps/mobile/src/i18n/locales/km.json | 16 ++ apps/mobile/src/i18n/locales/kn.json | 16 ++ apps/mobile/src/i18n/locales/ko.json | 16 ++ apps/mobile/src/i18n/locales/lo.json | 16 ++ apps/mobile/src/i18n/locales/lt.json | 16 ++ apps/mobile/src/i18n/locales/lv.json | 16 ++ apps/mobile/src/i18n/locales/mg.json | 16 ++ apps/mobile/src/i18n/locales/mi.json | 16 ++ apps/mobile/src/i18n/locales/mk.json | 16 ++ apps/mobile/src/i18n/locales/ml.json | 16 ++ apps/mobile/src/i18n/locales/mn.json | 16 ++ apps/mobile/src/i18n/locales/mr.json | 16 ++ apps/mobile/src/i18n/locales/ms.json | 16 ++ apps/mobile/src/i18n/locales/mt.json | 16 ++ apps/mobile/src/i18n/locales/my.json | 16 ++ apps/mobile/src/i18n/locales/nb.json | 16 ++ apps/mobile/src/i18n/locales/ne.json | 16 ++ apps/mobile/src/i18n/locales/nl.json | 16 ++ apps/mobile/src/i18n/locales/om.json | 16 ++ apps/mobile/src/i18n/locales/or.json | 16 ++ apps/mobile/src/i18n/locales/pa.json | 16 ++ apps/mobile/src/i18n/locales/pl.json | 16 ++ apps/mobile/src/i18n/locales/ps.json | 16 ++ apps/mobile/src/i18n/locales/pt-BR.json | 16 ++ apps/mobile/src/i18n/locales/pt.json | 16 ++ apps/mobile/src/i18n/locales/ro.json | 16 ++ apps/mobile/src/i18n/locales/ru.json | 16 ++ apps/mobile/src/i18n/locales/si.json | 16 ++ apps/mobile/src/i18n/locales/sk.json | 16 ++ apps/mobile/src/i18n/locales/sl.json | 16 ++ apps/mobile/src/i18n/locales/so.json | 16 ++ apps/mobile/src/i18n/locales/sq.json | 16 ++ apps/mobile/src/i18n/locales/sr.json | 16 ++ apps/mobile/src/i18n/locales/sv.json | 16 ++ apps/mobile/src/i18n/locales/sw.json | 16 ++ apps/mobile/src/i18n/locales/ta.json | 16 ++ apps/mobile/src/i18n/locales/te.json | 16 ++ apps/mobile/src/i18n/locales/th.json | 16 ++ apps/mobile/src/i18n/locales/tr.json | 16 ++ apps/mobile/src/i18n/locales/uk.json | 16 ++ apps/mobile/src/i18n/locales/ur.json | 16 ++ apps/mobile/src/i18n/locales/uz.json | 16 ++ apps/mobile/src/i18n/locales/vi.json | 16 ++ apps/mobile/src/i18n/locales/yo.json | 16 ++ apps/mobile/src/i18n/locales/zh-Hans.json | 16 ++ apps/mobile/src/i18n/locales/zh-Hant.json | 16 ++ apps/mobile/src/i18n/locales/zu.json | 16 ++ apps/web/src/lib/session-ingest-client.ts | 13 +- .../routers/cli-sessions-v2-router.test.ts | 23 +++ .../web/src/routers/cli-sessions-v2-router.ts | 17 +- packages/cloud-agent-sdk/src/index.ts | 4 +- .../cloud-agent-sdk/src/normalizer.test.ts | 131 ++++++++++++ packages/cloud-agent-sdk/src/normalizer.ts | 50 +++++ .../cloud-agent-sdk/src/service-state.test.ts | 50 +++++ packages/cloud-agent-sdk/src/service-state.ts | 30 ++- packages/cloud-agent-sdk/src/types.ts | 11 ++ packages/trpc/package.json | 1 + pnpm-lock.yaml | 3 + .../session-ingest/src/routes/api.test.ts | 71 +++++++ services/session-ingest/src/routes/api.ts | 41 ++++ 115 files changed, 2693 insertions(+), 25 deletions(-) create mode 100644 apps/mobile/src/components/agents/chat-composer-slash-commands-goal.test.ts create mode 100644 apps/mobile/src/components/agents/session-goal-actions.test.ts create mode 100644 apps/mobile/src/components/agents/session-goal-actions.ts create mode 100644 apps/mobile/src/components/agents/session-goal-section.tsx diff --git a/apps/mobile/src/components/agents/chat-composer-slash-commands-goal.test.ts b/apps/mobile/src/components/agents/chat-composer-slash-commands-goal.test.ts new file mode 100644 index 0000000000..39934ae0d0 --- /dev/null +++ b/apps/mobile/src/components/agents/chat-composer-slash-commands-goal.test.ts @@ -0,0 +1,186 @@ +import { describe, expect, it } from 'vitest'; + +import { type SlashCommandInfo } from '@kilocode/cloud-agent-sdk'; +import { type RemoteCommandState } from '@kilocode/cloud-agent-sdk/remote-command-catalog'; + +import { + isGoalCommandDraft, + parseChatComposerSubmission, +} from '@/components/agents/chat-composer-slash-commands'; + +const COMPACT: SlashCommandInfo = { name: 'compact', description: 'Compact', hints: [] }; +const GOAL: SlashCommandInfo = { name: 'goal', description: 'Goal', hints: [] }; +const WITHOUT_GOAL: SlashCommandInfo[] = [COMPACT]; +const WITH_GOAL: SlashCommandInfo[] = [COMPACT, GOAL]; + +function remoteState(overrides: Partial = {}): RemoteCommandState { + return { + ownerConnectionId: 'conn-1', + refresh: 'idle', + commands: WITH_GOAL, + ...overrides, + }; +} + +describe('parseChatComposerSubmission — /goal compose mode', () => { + it('routes a bare /goal to goal-compose', () => { + expect( + parseChatComposerSubmission('/goal', WITH_GOAL, { + hasAttachments: false, + sessionType: 'remote', + remoteCommandState: remoteState(), + }) + ).toEqual({ type: 'goal-compose' }); + }); + + it('routes /goal with only trailing whitespace to goal-compose', () => { + expect( + parseChatComposerSubmission('/goal ', WITH_GOAL, { + hasAttachments: false, + sessionType: 'remote', + remoteCommandState: remoteState(), + }) + ).toEqual({ type: 'goal-compose' }); + }); + + it('forwards /goal as the goal command', () => { + expect( + parseChatComposerSubmission('/goal Ship it', WITH_GOAL, { + hasAttachments: false, + sessionType: 'remote', + remoteCommandState: remoteState(), + }) + ).toEqual({ type: 'command', command: 'goal', arguments: 'Ship it' }); + }); + + it.each(['pause', 'resume', 'clear'])('forwards /goal %s as the goal command', argument => { + expect( + parseChatComposerSubmission(`/goal ${argument}`, WITH_GOAL, { + hasAttachments: false, + sessionType: 'remote', + remoteCommandState: remoteState(), + }) + ).toEqual({ type: 'command', command: 'goal', arguments: argument }); + }); + + it('forwards /goal for a cloud-agent session whose catalog advertises it', () => { + expect( + parseChatComposerSubmission('/goal Ship it', WITH_GOAL, { + hasAttachments: false, + sessionType: 'cloud-agent', + remoteCommandState: null, + }) + ).toEqual({ type: 'command', command: 'goal', arguments: 'Ship it' }); + }); + + it('rejects a goal command with attachments', () => { + expect( + parseChatComposerSubmission('/goal Ship it', WITH_GOAL, { + hasAttachments: true, + sessionType: 'remote', + remoteCommandState: remoteState(), + }) + ).toEqual({ type: 'attachment-error' }); + }); + + it('rejects a bare /goal with attachments before entering compose mode', () => { + expect( + parseChatComposerSubmission('/goal', WITH_GOAL, { + hasAttachments: true, + sessionType: 'remote', + remoteCommandState: remoteState(), + }) + ).toEqual({ type: 'attachment-error' }); + }); +}); + +describe('isGoalCommandDraft', () => { + it.each(['/goal', '/goal ', '/goal Ship it', '/goal Ship it'])( + 'keeps compose mode for the goal draft %j', + draft => { + expect(isGoalCommandDraft(draft)).toBe(true); + } + ); + + it.each(['', '/go', '/goalx', 'hello', '/ goals'])( + 'drops compose mode for the non-goal draft %j', + draft => { + expect(isGoalCommandDraft(draft)).toBe(false); + } + ); +}); + +describe('parseChatComposerSubmission — /goal fail-closed', () => { + it('returns upgrade-required (never a prompt) for a remote catalog without goal', () => { + const result = parseChatComposerSubmission('/goal Ship it', WITHOUT_GOAL, { + hasAttachments: false, + sessionType: 'remote', + remoteCommandState: remoteState({ commands: WITHOUT_GOAL }), + }); + expect(result).toEqual({ + type: 'upgrade-required', + message: 'Please upgrade your CLI to use this command.', + }); + }); + + it('returns upgrade-required (never a prompt) for a cloud-agent catalog without goal', () => { + expect( + parseChatComposerSubmission('/goal Ship it', WITHOUT_GOAL, { + hasAttachments: false, + sessionType: 'cloud-agent', + remoteCommandState: null, + }) + ).toEqual({ + type: 'upgrade-required', + message: 'Please upgrade your CLI to use this command.', + }); + }); + + it('returns upgrade-required for a bare /goal when the remote catalog lacks goal', () => { + expect( + parseChatComposerSubmission('/goal', WITHOUT_GOAL, { + hasAttachments: false, + sessionType: 'remote', + remoteCommandState: remoteState({ commands: WITHOUT_GOAL }), + }) + ).toEqual({ + type: 'upgrade-required', + message: 'Please upgrade your CLI to use this command.', + }); + }); + + it('returns upgrade-required for a remote session requiring an upgrade', () => { + expect( + parseChatComposerSubmission('/goal Ship it', WITH_GOAL, { + hasAttachments: false, + sessionType: 'remote', + remoteCommandState: remoteState({ + refresh: 'upgrade-required', + message: 'Please upgrade your CLI', + }), + }) + ).toEqual({ type: 'upgrade-required', message: 'Please upgrade your CLI' }); + }); +}); + +describe('parseChatComposerSubmission — non-interactive sessions keep /goal as a prompt', () => { + it('keeps /goal as a prompt for a read-only session', () => { + expect( + parseChatComposerSubmission('/goal Ship it', WITHOUT_GOAL, { + hasAttachments: false, + sessionType: 'read-only', + remoteCommandState: null, + }) + ).toEqual({ type: 'prompt', prompt: '/goal Ship it' }); + }); + + it('keeps /goal as a prompt for an unresolved session', () => { + expect( + parseChatComposerSubmission('/goal Ship it', WITHOUT_GOAL, { + hasAttachments: false, + sessionType: null, + remoteCommandState: null, + }) + ).toEqual({ type: 'prompt', prompt: '/goal Ship it' }); + }); +}); diff --git a/apps/mobile/src/components/agents/chat-composer-slash-commands.test.ts b/apps/mobile/src/components/agents/chat-composer-slash-commands.test.ts index 77d6453cd2..25617152e4 100644 --- a/apps/mobile/src/components/agents/chat-composer-slash-commands.test.ts +++ b/apps/mobile/src/components/agents/chat-composer-slash-commands.test.ts @@ -14,6 +14,7 @@ import { const COMPACT: SlashCommandInfo = { name: 'compact', description: 'Compact', hints: [] }; const REVIEW: SlashCommandInfo = { name: 'review', description: 'Review', hints: [] }; +const GOAL: SlashCommandInfo = { name: 'goal', description: 'Goal', hints: [] }; const SAMPLE_COMMANDS: SlashCommandInfo[] = [COMPACT, REVIEW]; function remoteState(overrides: Partial = {}): RemoteCommandState { @@ -77,6 +78,11 @@ describe('createMobileSlashCommandList', () => { expect(list).toBe(SAMPLE_COMMANDS); }); + it('does not strip a CLI-reported /goal from a remote catalog', () => { + const list = createMobileSlashCommandList('remote', [GOAL], remoteState({ commands: [GOAL] })); + expect(list.map(command => command.name)).toEqual(['goal', 'new']); + }); + it('exposes no commands for read-only, unresolved, or other noninteractive session types', () => { expect(createMobileSlashCommandList('read-only', SAMPLE_COMMANDS, null)).toEqual([]); expect(createMobileSlashCommandList(null, SAMPLE_COMMANDS, null)).toEqual([]); diff --git a/apps/mobile/src/components/agents/chat-composer-slash-commands.ts b/apps/mobile/src/components/agents/chat-composer-slash-commands.ts index 9595d46347..5a096c5ad9 100644 --- a/apps/mobile/src/components/agents/chat-composer-slash-commands.ts +++ b/apps/mobile/src/components/agents/chat-composer-slash-commands.ts @@ -42,6 +42,7 @@ export function getLocalClearSlashCommand(): SlashCommandInfo { const NEW_COMMAND_NAME = 'new'; const EXIT_COMMAND_NAME = 'exit'; const CLEAR_COMMAND_NAME = 'clear'; +const GOAL_COMMAND_NAME = 'goal'; const LOCAL_COMMAND_NAMES = new Set([ NEW_COMMAND_NAME, EXIT_COMMAND_NAME, @@ -63,12 +64,17 @@ const SLASH_FULL_PATTERN = /^\/([\w.-]+)(?:\s+([\s\S]*))?$/; * `/clear` is intentionally gated — it needs both create_session and * exit_cli and must surface the upgrade message instead of falling through * as a prompt. + * + * `/goal ...` is included so that on a remote CLI that reports + * `refresh: 'upgrade-required'` the mobile composer returns the upgrade + * message instead of forwarding goal text as an ordinary prompt. */ const RESERVED_UPGRADE_REQUIRED_COMMANDS = new Set([ 'compact', NEW_COMMAND_NAME, EXIT_COMMAND_NAME, CLEAR_COMMAND_NAME, + GOAL_COMMAND_NAME, ]); type ChatComposerParseContext = { @@ -83,6 +89,7 @@ export type ChatComposerParseResult = | { type: 'create-session' } | { type: 'exit-session' } | { type: 'restart-session' } + | { type: 'goal-compose' } | { type: 'attachment-error' } | { type: 'argument-error'; message: string } | { type: 'upgrade-required'; message: string }; @@ -134,6 +141,16 @@ export function getSlashCommandCandidate(input: string): string | null { return SLASH_PREFIX_PATTERN.test(input) ? input : null; } +/** + * True while `input` is still the `/goal` compose draft (bare `/goal` or + * `/goal `). The composer uses this to keep its goal compose mode + * alive as the user types the objective and to drop it the moment the draft is + * no longer about the goal command. + */ +export function isGoalCommandDraft(input: string): boolean { + return /^\/goal(?:\s|$)/.test(input); +} + /** * Return the catalog entries whose name starts with the prefix in `input`. * Returns `[]` for anything that is not still a slash-name candidate. @@ -269,6 +286,32 @@ export function parseChatComposerSubmission( return { type: 'restart-session' }; } + if ( + commandName === GOAL_COMMAND_NAME && + (context.sessionType === 'remote' || context.sessionType === 'cloud-agent') + ) { + // /goal is only supported when the session's catalog advertises it. Fail + // closed on a session that does not, so goal text is never sent as + // ordinary chat; the existing fail-closed copy is reused so no new i18n + // key is introduced. `null` and `read-only` sessions are excluded above + // and keep their existing prompt behavior. + if (!findCommand(commands, GOAL_COMMAND_NAME)) { + return { + type: 'upgrade-required', + message: i18n.t('agentChat.slashCommands.upgradeRequiredFallback'), + }; + } + if (context.hasAttachments) { + return { type: 'attachment-error' }; + } + if (argumentsText === '') { + // Bare `/goal` enters compose mode; selecting `/goal` from the + // suggestion list inserts `/goal ` and lands here. + return { type: 'goal-compose' }; + } + return { type: 'command', command: GOAL_COMMAND_NAME, arguments: argumentsText }; + } + if (commandName && findCommand(commands, commandName)) { if (context.hasAttachments) { return { type: 'attachment-error' }; diff --git a/apps/mobile/src/components/agents/chat-composer.test.ts b/apps/mobile/src/components/agents/chat-composer.test.ts index 06b49b05d3..f5d2db3d36 100644 --- a/apps/mobile/src/components/agents/chat-composer.test.ts +++ b/apps/mobile/src/components/agents/chat-composer.test.ts @@ -8,6 +8,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { type AgentMode } from '@/components/agents/mode-selector'; import { Text as renderText } from '@/components/ui/text'; import { CLOUD_AGENT_PROMPT_MAX_LENGTH } from '@kilocode/cloud-agent-sdk/limits'; +import { type SlashCommandInfo } from '@kilocode/cloud-agent-sdk'; +import { type RemoteCommandState } from '@kilocode/cloud-agent-sdk/remote-command-catalog'; import { type ChatComposer } from './chat-composer'; const layoutDirection = vi.hoisted(() => ({ isRTL: false })); @@ -124,8 +126,14 @@ vi.mock('@/lib/navigation/prevent-remove', () => ({ usePreventRemove: vi.fn(), })); +// Identity-matched so a test can read the goal compose hint's message. The +// real component owns the announcement channel; the composer only needs to +// render it with the right message. Deliberately not `__testMarker`-tagged so +// `findInputRowProps` cannot mistake it for the input row. +const MockAccessibleStatus = () => null; + vi.mock('@/components/ui/accessible-status', () => ({ - AccessibleStatus: () => null, + AccessibleStatus: MockAccessibleStatus, })); vi.mock('react-native-reanimated', () => ({ @@ -640,6 +648,90 @@ describe('ChatComposer return-sends wiring', () => { }); }); +describe('ChatComposer goal compose mode', () => { + const GOAL_COMMAND: SlashCommandInfo = { name: 'goal', description: 'Goal', hints: [] }; + + function remoteGoalProps(overrides: Partial = {}): ComposerProps { + const commandState: RemoteCommandState = { + ownerConnectionId: 'conn-1', + refresh: 'idle', + commands: [GOAL_COMMAND], + }; + return makeProps({ + activeSessionType: 'remote', + commandState, + ...overrides, + }); + } + + async function enterGoalComposeMode( + props: ComposerProps, + draft = '/goal ' + ): Promise { + const render = await mount(props); + requireInputRowOnChangeText(render)(draft); + await settle(); + requireInputRowOnSubmit(render)(); + await settle(); + return rerender(props); + } + + it('enters goal compose mode and shows the objective hint on a bare /goal send', async () => { + const onSendCommand = vi.fn(async () => true); + const props = remoteGoalProps({ onSendCommand }); + const after = await enterGoalComposeMode(props); + + const hint = findNode(after, type => type === MockAccessibleStatus); + expect(hint?.props).toMatchObject({ message: 'Describe the goal', tone: 'status' }); + // The draft is kept so the user can complete the goal command. + expect(refSlots.slots[0]?.current).toBe('/goal '); + expect(onSendCommand).not.toHaveBeenCalled(); + expect(onSendMock).not.toHaveBeenCalled(); + }); + + it('normalizes a separator-less /goal draft so the next keystroke stays a goal command', async () => { + const onSendCommand = vi.fn(async () => true); + const props = remoteGoalProps({ onSendCommand }); + const after = await enterGoalComposeMode(props, '/goal'); + + // The retained draft ends with the separator, so the next keystroke + // continues the goal command instead of producing `/goalShip it`. + const retained = refSlots.slots[0]?.current as string; + expect(retained).toBe('/goal '); + + requireInputRowOnChangeText(after)(`${retained}Ship it`); + await settle(); + requireInputRowOnSubmit(after)(); + await settle(); + + expect(onSendCommand).toHaveBeenCalledWith('goal', 'Ship it'); + }); + + it('forwards the objective and leaves compose mode on the next send', async () => { + const onSendCommand = vi.fn(async () => true); + const props = remoteGoalProps({ onSendCommand }); + const render = await enterGoalComposeMode(props); + + requireInputRowOnChangeText(render)('/goal Ship it'); + await settle(); + requireInputRowOnSubmit(render)(); + await settle(); + + expect(onSendCommand).toHaveBeenCalledWith('goal', 'Ship it'); + const after = await rerender(props); + expect(findNode(after, type => type === MockAccessibleStatus)).toBeNull(); + }); + + it('drops the hint when the draft is no longer the goal command', async () => { + const props = remoteGoalProps(); + const render = await enterGoalComposeMode(props); + + requireInputRowOnChangeText(render)('write the docs'); + const after = await rerender(props); + expect(findNode(after, type => type === MockAccessibleStatus)).toBeNull(); + }); +}); + describe('ChatComposer CLI suggestion', () => { it('renders the suggestion in the composer and wires both actions', async () => { const onAcceptSuggestion = vi.fn(async () => undefined); diff --git a/apps/mobile/src/components/agents/chat-composer.tsx b/apps/mobile/src/components/agents/chat-composer.tsx index ea722d6f49..ac6506f4f3 100644 --- a/apps/mobile/src/components/agents/chat-composer.tsx +++ b/apps/mobile/src/components/agents/chat-composer.tsx @@ -49,6 +49,7 @@ import { createMobileSlashCommandList, getSlashCommandCandidate, getSlashCommandSuggestions, + isGoalCommandDraft, parseChatComposerSubmission, } from '@/components/agents/chat-composer-slash-commands'; import { executeChatComposerSubmission } from '@/components/agents/chat-composer-submission'; @@ -281,6 +282,10 @@ export function ChatComposer({ const [characterCount, setCharacterCount] = useState(0); const [keyboardHeight, setKeyboardHeight] = useState(0); const [slashCommandInput, setSlashCommandInput] = useState(null); + // True after a bare `/goal` submission: the composer is collecting the goal + // objective. Keeps the `/goal ` draft and surfaces the objective hint so the + // next step is explicit instead of the send silently doing nothing. + const [goalComposeActive, setGoalComposeActive] = useState(false); const [inputWidth, setInputWidth] = useState(0); const [isFocused, setIsFocused] = useState(false); const [isSending, setIsSending] = useState(false); @@ -457,6 +462,9 @@ export function ChatComposer({ setHasText(draft.trim().length > 0); setCharacterCount(draft.length); setSlashCommandInput(getSlashCommandCandidate(draft)); + if (!isGoalCommandDraft(draft)) { + setGoalComposeActive(false); + } measureRef.current.setText(draft); }, []); @@ -532,6 +540,9 @@ export function ChatComposer({ setHasText(value.trim().length > 0); setCharacterCount(value.length); setSlashCommandInput(null); + if (!isGoalCommandDraft(value)) { + setGoalComposeActive(false); + } inputRef.current?.setNativeProps({ text: value, selection: { start: value.length, end: value.length }, @@ -574,6 +585,12 @@ export function ChatComposer({ function handleChangeText(value: string) { textRef.current = value; + // A draft that is no longer the `/goal` command ends goal compose mode, so + // the objective hint never outlives the text it describes. A no-op when the + // mode is already off. + if (!isGoalCommandDraft(value)) { + setGoalComposeActive(false); + } // Derived state (measure node, hasText, slash command) is coalesced to one // publication per frame; the live submit-time ref and the debounced draft // write stay synchronous so neither can lag a keystroke. @@ -860,6 +877,7 @@ export function ChatComposer({ setHasText(false); setCharacterCount(0); setSlashCommandInput(null); + setGoalComposeActive(false); measure.reset(); inputRef.current?.clear(); // Clear the persisted draft only on successful send / explicit clear, @@ -904,6 +922,21 @@ export function ChatComposer({ toast.error(submission.message); return; } + if (submission.type === 'goal-compose') { + // Bare `/goal` is a compose mode: keep the draft, mark the composer so the + // objective hint appears, and focus the input so the user can type the + // objective. Normalize the retained draft to `/goal ` first — a bare + // `/goal` with no separator would put the next keystroke directly after + // `goal` (`/goalShip it`), which is no longer a goal command and would be + // sent as an ordinary prompt. `applyComposerText` moves the caret to the + // end and focuses the input. The next send parses as `/goal ` + // and forwards to the CLI. + setGoalComposeActive(true); + applyComposerText(`${trimmed} `); + return; + } + // Any other submission leaves goal compose mode. + setGoalComposeActive(false); // The admission lock is owned by `settleVoiceInputBeforeSubmit` for the // full settle + submit sequence, so `handleSend` performs validation and @@ -1216,6 +1249,14 @@ export function ChatComposer({ + {goalComposeActive ? ( + + ) : null} + {CLOUD_AGENT_PROMPT_MAX_LENGTH - characterCount <= COMPOSER_COUNTER_VISIBLE_REMAINING ? ( ; + const snapshotInfo = messagesResult.info as Partial & { + metadata?: unknown; + }; + // The goal lives in the session metadata; project it into `info` so the + // fixed goal section survives a snapshot replay (which would otherwise + // clobber the goal carried by the live session state). + const goal = projectSessionGoal(snapshotInfo.metadata); return { info: { id: snapshotInfo.id ?? sessionData.session_id, parentID: snapshotInfo.parentID ?? sessionData.parent_session_id ?? undefined, ...(snapshotInfo.model ? { model: snapshotInfo.model } : {}), + ...(goal === undefined ? {} : { goal }), }, messages: messagesResult.messages as SessionSnapshot['messages'], }; diff --git a/apps/mobile/src/components/agents/mobile-session-page-adapter.test.ts b/apps/mobile/src/components/agents/mobile-session-page-adapter.test.ts index d688429c28..dc8c3140d0 100644 --- a/apps/mobile/src/components/agents/mobile-session-page-adapter.test.ts +++ b/apps/mobile/src/components/agents/mobile-session-page-adapter.test.ts @@ -242,6 +242,40 @@ describe('fetchMobileSessionSnapshotPage', () => { expect(call.limit).toBeUndefined(); }); + it('projects the session goal from the messages page into the snapshot info', async () => { + const goal = { text: 'Ship p7 objective', status: 'paused' as const }; + mocks.getSessionMessagesPageQuery.mockResolvedValueOnce({ + kiloSessionId: 'ses_123', + history: historyPage({ nextCursor: null }), + sessionGoal: goal, + }); + + const { fetchMobileSessionSnapshotPage } = await importAdapter(); + const result = await fetchMobileSessionSnapshotPage(kiloSessionId('ses_123'), {}); + + // The transport replays this `info` as `session.created`, so the goal + // must be present or the fixed goal section is lost on open/reconnect. + expect(result).toMatchObject({ kind: 'success', info: { id: 'ses_123', goal } }); + }); + + it('carries the goal into an empty (history:null) snapshot page', async () => { + const goal = { text: 'Ship p7 objective', status: 'paused' as const }; + mocks.getSessionMessagesPageQuery.mockResolvedValueOnce({ + kiloSessionId: 'ses_empty', + history: null, + sessionGoal: goal, + }); + + const { fetchMobileSessionSnapshotPage } = await importAdapter(); + const result = await fetchMobileSessionSnapshotPage(kiloSessionId('ses_empty'), {}); + + expect(result).toMatchObject({ + kind: 'success', + info: { id: 'ses_empty', goal }, + messages: [], + }); + }); + it('carries watermarkEventId through when the web router includes it', async () => { mocks.getSessionMessagesPageQuery.mockResolvedValueOnce({ kiloSessionId: 'ses_123', diff --git a/apps/mobile/src/components/agents/mobile-session-page-adapter.ts b/apps/mobile/src/components/agents/mobile-session-page-adapter.ts index 2122172894..2d599e0444 100644 --- a/apps/mobile/src/components/agents/mobile-session-page-adapter.ts +++ b/apps/mobile/src/components/agents/mobile-session-page-adapter.ts @@ -38,11 +38,21 @@ export async function fetchMobileSessionSnapshotPage( ...(options.cursor ? { cursor: options.cursor } : {}), }); + // The goal lives in the session metadata, which the messages page carries + // alongside the bounded history. Rebuilding `info` from the page (instead + // of `{ id }`) keeps the fixed goal section across open and reconnect: the + // transport replays this `info` as `session.created`, which otherwise + // clobbers the goal held in the live session state. + const info: SessionSnapshotPage['info'] = { + id: result.kiloSessionId, + ...(result.sessionGoal ? { goal: result.sessionGoal } : {}), + }; + const history = result.history as KiloSdkMessageHistory | null; if (history === null) { return { kind: 'success', - info: { id: result.kiloSessionId }, + info, messages: [], nextCursor: null, omittedItemCount: 0, @@ -53,7 +63,7 @@ export async function fetchMobileSessionSnapshotPage( if (isHistoryPage(history)) { return { kind: 'success', - info: { id: result.kiloSessionId }, + info, messages: history.messages as SessionSnapshotPage['messages'], nextCursor: history.nextCursor, omittedItemCount: history.omittedItemCount, diff --git a/apps/mobile/src/components/agents/session-detail-content.test.ts b/apps/mobile/src/components/agents/session-detail-content.test.ts index 28ac691ea7..814af354c7 100644 --- a/apps/mobile/src/components/agents/session-detail-content.test.ts +++ b/apps/mobile/src/components/agents/session-detail-content.test.ts @@ -10,6 +10,7 @@ import { createSessionManager, createUserWebConnection, type KiloSessionId, + type SessionGoal, type SessionManager, type SessionSnapshotPageOutcome, type SessionStatusIndicator, @@ -24,6 +25,7 @@ import { ChildSessionSheet } from '@/components/agents/child-session-sheet'; import { getTaskToolSessionId } from '@/components/agents/child-session-card-state'; import { assistantMessage } from '@/components/agents/message-bubble-test-utils'; import { SessionDetailContent } from '@/components/agents/session-detail-content'; +import { SessionGoalSection } from '@/components/agents/session-goal-section'; import { SessionSkeletonMessages } from '@/components/agents/session-detail-skeleton'; import { type SessionMessageList } from '@/components/agents/session-message-list'; import { @@ -108,6 +110,7 @@ vi.mock('sonner-native', () => ({ toast: { error: vi.fn() } })); vi.mock('@/components/ui/icons', () => ({ Bot: 'Bot', ChevronDown: 'ChevronDown', + CircleDot: 'CircleDot', Clock: 'Clock', Loader2: 'Loader2', MessageSquare: 'MessageSquare', @@ -301,6 +304,11 @@ const globalContext = vi.hoisted(() => ({ vi.mock('@/lib/organization-context', () => ({ useOrganization: () => globalContext })); const PERSONAL_DISPLAY_SCOPE = { organizationId: null, isResolved: true }; +/** + * Goal/type overrides for the goal-visibility tests. A module-level slot keeps + * the shared `mountDetails` fixture at its existing three-parameter signature. + */ +let goalMountOptions: { goal?: SessionGoal; resolvedType?: 'read-only' | 'remote' } = {}; const ROOT_ID = kiloId('ses-root'); const NEXT_ROOT_ID = kiloId('ses-next-root'); const SELECTED_ID = kiloId('ses-selected'); @@ -364,10 +372,14 @@ function childMessage(sessionId: KiloSessionId, text: string): StoredMessage { }; } -function page(sessionId: KiloSessionId, messages: StoredMessage[]): SessionSnapshotPageOutcome { +function page( + sessionId: KiloSessionId, + messages: StoredMessage[], + goal?: SessionGoal +): SessionSnapshotPageOutcome { return { kind: 'success', - info: { id: sessionId }, + info: { id: sessionId, ...(goal ? { goal } : {}) }, messages, nextCursor: null, omittedItemCount: 0, @@ -377,6 +389,7 @@ function page(sessionId: KiloSessionId, messages: StoredMessage[]): SessionSnaps beforeEach(() => { navigationRoutes.splice(0, navigationRoutes.length, 'session-detail'); openRenameModal.mockClear(); + goalMountOptions = {}; globalContext.organizationId = 'global-org'; globalContext.setOrganizationId.mockClear(); }); @@ -401,6 +414,9 @@ async function mountDetails( userWebConnection: connection, resolveSession: async id => { await Promise.resolve(); + if (goalMountOptions.resolvedType === 'remote') { + return { type: 'remote', kiloSessionId: id }; + } return { type: 'read-only', kiloSessionId: id }; }, getTicket: vi.fn(), @@ -410,7 +426,7 @@ async function mountDetails( requests.push({ id, response }); const messages = rootPages.get(id); if (messages) { - response.resolve(page(id, messages)); + response.resolve(page(id, messages, goalMountOptions.goal)); } const outcome = await response.promise; return outcome; @@ -474,7 +490,7 @@ async function mountDetails( requestedIds: () => requests.map(request => request.id), respond: async (id: KiloSessionId, messages: StoredMessage[]) => { await act(async () => { - requestFor(id).resolve(page(id, messages)); + requestFor(id).resolve(page(id, messages, goalMountOptions.goal)); await Promise.resolve(); }); }, @@ -969,3 +985,21 @@ describe('child transcript requests', () => { expect(view.requestedIds()).toEqual([ROOT_ID]); }); }); + +describe('SessionDetailContent goal visibility', () => { + const pausedGoal: SessionGoal = { text: 'Ship p7 objective', status: 'paused' }; + + it('shows the fixed goal row for a live session whose snapshot carries a goal', async () => { + goalMountOptions = { goal: pausedGoal, resolvedType: 'remote' }; + const view = await mountDetails([], undefined, PERSONAL_DISPLAY_SCOPE); + const section = view.renderer.root.findAllByType(SessionGoalSection); + expect(section).toHaveLength(1); + expect(section[0]?.props.goal).toEqual(pausedGoal); + }); + + it('hides the fixed goal row for a read-only session whose snapshot carries a goal', async () => { + goalMountOptions = { goal: pausedGoal, resolvedType: 'read-only' }; + const view = await mountDetails([], undefined, PERSONAL_DISPLAY_SCOPE); + expect(view.renderer.root.findAllByType(SessionGoalSection)).toHaveLength(0); + }); +}); diff --git a/apps/mobile/src/components/agents/session-detail-content.tsx b/apps/mobile/src/components/agents/session-detail-content.tsx index 6578897c9d..fdbac94f08 100644 --- a/apps/mobile/src/components/agents/session-detail-content.tsx +++ b/apps/mobile/src/components/agents/session-detail-content.tsx @@ -4,13 +4,14 @@ import { type MessageDeliveryState, type StoredMessage, } from '@kilocode/cloud-agent-sdk'; +import { useActionSheet } from '@expo/react-native-action-sheet'; import { type Href, useFocusEffect, useIsFocused, useRouter } from 'expo-router'; import { useAtomValue, useSetAtom, useStore } from 'jotai'; import { MessageSquare } from '@/components/ui/icons'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useKeepAwake } from 'expo-keep-awake'; import * as Haptics from 'expo-haptics'; -import { KeyboardAvoidingView, Platform, type Text as RNText, View } from 'react-native'; +import { Alert, KeyboardAvoidingView, Platform, type Text as RNText, View } from 'react-native'; import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTranslation } from 'react-i18next'; @@ -47,6 +48,14 @@ import { } from '@/components/agents/context-usage-display'; import { resolveSessionComposerDisabled } from '@/components/agents/session-composer-disabled'; import { SessionConnectionIndicator } from '@/components/agents/session-connection-indicator'; +import { + type GoalAction, + goalClearsBlockingAfterSend, + goalCommandArguments, + resolveGoalActions, + selectVisibleGoal, +} from '@/components/agents/session-goal-actions'; +import { SessionGoalSection } from '@/components/agents/session-goal-section'; import { SessionContextMetrics } from '@/components/agents/session-context-metrics'; import { SessionContextSheet } from '@/components/agents/session-context-sheet'; import { SessionPrBadge } from '@/components/agents/session-pr-badge'; @@ -150,6 +159,13 @@ import { import { trpcClient } from '@/lib/trpc'; import { cn } from '@/lib/utils'; +const GOAL_ACTION_LABEL_KEY = { + edit: 'agentChat.goal.edit', + pause: 'agentChat.goal.pause', + resume: 'agentChat.goal.resume', + remove: 'agentChat.goal.remove', +} as const satisfies Record; + type SessionDetailContentProps = { sessionId: KiloSessionId; displayScope: ContextDisplayScope; @@ -234,6 +250,8 @@ export function SessionDetailContent({ const remoteModelOverride = useAtomValue(manager.atoms.remoteModelOverride); const cloudAgentModelOverride = useAtomValue(manager.atoms.cloudAgentModelOverride); const availableCommands = useAtomValue(manager.atoms.availableCommands); + const sessionInfo = useAtomValue(manager.atoms.sessionInfo); + const sessionGoal = selectVisibleGoal(sessionInfo, isReadOnly); const remoteCommandState = useAtomValue(manager.atoms.remoteCommandState); const contextUsage = useAtomValue(manager.atoms.contextUsage); const hasOlderMessages = useAtomValue(manager.atoms.hasOlderMessages); @@ -244,8 +262,10 @@ export function SessionDetailContent({ useState(null); const [detailsMessageId, setDetailsMessageId] = useState(null); const detailsMessageIdRef = useRef(null); + const [isGoalEditOpen, setIsGoalEditOpen] = useState(false); const { bottom } = useSafeAreaInsets(); + const { showActionSheetWithOptions } = useActionSheet(); // Durable composer draft. The composer renders immediately — typing must // never wait on the `user.getMe` query — and the draft load settles behind @@ -1287,6 +1307,133 @@ export function SessionDetailContent({ [manager] ); + // Goal controls ride the same `manager.send()` command pipeline as the + // composer's slash commands. The manager is the sole transport-toast owner, + // so a failed send surfaces exactly one error toast from `onSendFailed` and + // this helper never adds a second. Edit throws instead, so the RenameModal + // shows the failure inline and the user can correct the objective. One + // helper keeps the `/goal` payload shape in one place. + const sendGoalAction = useCallback( + async (action: GoalAction, objective = ''): Promise => { + const sent = await manager.send({ + payload: { + type: 'command', + command: 'goal', + arguments: goalCommandArguments(action, objective), + }, + }); + return sent; + }, + [manager] + ); + + // The CLI rejects `/goal resume` and `/goal ` while a question or + // permission is pending, and it records a *rejected* request as a blocked + // goal while a goal is running. Pause and clear are accepted while a request + // is pending, so they clear it after the goal stops (the stopped goal no + // longer observes the rejection); resume and edit clear it before sending. + // Without this a paused goal's pending request is stranded and Resume + // silently no-ops. The interaction handlers own their own failure toast, so + // a failed clear only aborts the goal command. + const clearGoalBlockingInteraction = useCallback(async (): Promise => { + if (blockingInteraction === 'question') { + const cleared = await handleRejectQuestion(); + return cleared; + } + if (blockingInteraction === 'permission') { + const cleared = await handleRespondToPermission('reject'); + return cleared; + } + return true; + }, [blockingInteraction, handleRejectQuestion, handleRespondToPermission]); + + const runGoalAction = useCallback( + async (action: GoalAction) => { + if (!hasBlockingInteraction) { + await sendGoalAction(action); + return; + } + if (goalClearsBlockingAfterSend(action)) { + // Pause/clear first: the CLI accepts them while a request is pending, + // and stopping the goal keeps the later rejection from marking it + // blocked. + const sent = await sendGoalAction(action); + if (sent) { + await clearGoalBlockingInteraction(); + } + return; + } + // Resume/edit are rejected until the request is cleared. + if (!(await clearGoalBlockingInteraction())) { + return; + } + await sendGoalAction(action); + }, + [hasBlockingInteraction, clearGoalBlockingInteraction, sendGoalAction] + ); + + const handleOpenGoalActions = useCallback(() => { + if (!sessionGoal) { + return; + } + const actions = resolveGoalActions(sessionGoal); + const options = [ + ...actions.map(action => t(GOAL_ACTION_LABEL_KEY[action])), + t('common.cancel'), + ]; + const removeIndex = actions.indexOf('remove'); + showActionSheetWithOptions( + { + title: t('agentChat.goal.title'), + options, + cancelButtonIndex: options.length - 1, + destructiveButtonIndex: removeIndex === -1 ? undefined : removeIndex, + containerStyle: { paddingBottom: bottom }, + }, + index => { + const action = index === undefined ? undefined : actions[index]; + if (!action) { + return; + } + if (action === 'edit') { + setIsGoalEditOpen(true); + return; + } + if (action === 'remove') { + Alert.alert( + t('agentChat.goal.removeConfirmTitle'), + t('agentChat.goal.removeConfirmMessage'), + [ + { text: t('common.cancel'), style: 'cancel' }, + { + text: t('agentChat.goal.remove'), + style: 'destructive', + onPress: () => { + void runGoalAction('remove'); + }, + }, + ] + ); + return; + } + void runGoalAction(action); + } + ); + }, [sessionGoal, t, showActionSheetWithOptions, bottom, runGoalAction]); + + const handleGoalEditSave = useCallback( + async (objective: string) => { + if (!(await clearGoalBlockingInteraction())) { + throw new Error(t('agentChat.goal.updateFailed')); + } + const sent = await sendGoalAction('edit', objective); + if (!sent) { + throw new Error(t('agentChat.goal.updateFailed')); + } + }, + [clearGoalBlockingInteraction, sendGoalAction, t] + ); + const handleCreateSession = useCallback(async () => { // The orchestrator surfaces exactly one actionable toast on failure and // calls `router.replace` to the new session route on success — the @@ -1410,6 +1557,15 @@ export function SessionDetailContent({ activeSessionType={activeSessionType} agentStatusType={agentStatus.type} /> + {sessionGoal ? ( + + + + ) : null} {keepScreenAwake ? : null} {keyboardContainerKind === 'app-aware-padding' ? ( @@ -1508,6 +1664,19 @@ export function SessionDetailContent({ onClose={handleRenameClose} /> ) : null} + + {sessionGoal && isGoalEditOpen ? ( + { + setIsGoalEditOpen(false); + }} + /> + ) : null} ); diff --git a/apps/mobile/src/components/agents/session-detail-queue.test.ts b/apps/mobile/src/components/agents/session-detail-queue.test.ts index 40b489cb0b..7f3a3d0f99 100644 --- a/apps/mobile/src/components/agents/session-detail-queue.test.ts +++ b/apps/mobile/src/components/agents/session-detail-queue.test.ts @@ -42,6 +42,9 @@ const hoisted = vi.hoisted(() => { // Mock every RN / Expo / SDK side-effect import that `mobile-session-manager.ts` // and `session-detail-content.tsx` pull in transitively before loading either // module. +vi.mock('@expo/react-native-action-sheet', () => ({ + useActionSheet: () => ({ showActionSheetWithOptions: vi.fn() }), +})); vi.mock('@/components/centered-state', () => ({ CenteredState: 'CenteredState' })); vi.mock('@/components/centered-state-surface', () => ({ StateSurface: 'StateSurface' })); vi.mock('expo-secure-store', () => ({ @@ -497,6 +500,7 @@ function makeManager() { remoteModelOverride: { value: null }, cloudAgentModelOverride: { value: null }, availableCommands: { value: [] }, + sessionInfo: { value: null }, remoteCommandState: { value: null }, contextUsage: { value: null }, hasOlderMessages: { value: false }, diff --git a/apps/mobile/src/components/agents/session-goal-actions.test.ts b/apps/mobile/src/components/agents/session-goal-actions.test.ts new file mode 100644 index 0000000000..b098f0583e --- /dev/null +++ b/apps/mobile/src/components/agents/session-goal-actions.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest'; + +import { + type SessionGoal, + type SessionGoalStatus, + type SessionInfo, +} from '@kilocode/cloud-agent-sdk'; + +import { + goalClearsBlockingAfterSend, + goalCommandArguments, + resolveGoalActions, + selectVisibleGoal, +} from './session-goal-actions'; + +function goal(status: SessionGoalStatus): SessionGoal { + return { text: 'Ship the release', status }; +} + +describe('resolveGoalActions', () => { + it('offers edit, pause, and remove for an active goal', () => { + expect(resolveGoalActions(goal('active'))).toEqual(['edit', 'pause', 'remove']); + }); + + it('offers edit, resume, and remove for a paused goal', () => { + expect(resolveGoalActions(goal('paused'))).toEqual(['edit', 'resume', 'remove']); + }); + + it('offers edit, resume, and remove for a complete goal so it can be restarted', () => { + expect(resolveGoalActions(goal('complete'))).toEqual(['edit', 'resume', 'remove']); + }); + + it('offers edit, resume, and remove for a blocked goal once the blocker is resolved', () => { + expect(resolveGoalActions(goal('blocked'))).toEqual(['edit', 'resume', 'remove']); + }); +}); + +describe('goalClearsBlockingAfterSend', () => { + it('clears a pending request after pause, once the goal has stopped', () => { + expect(goalClearsBlockingAfterSend('pause')).toBe(true); + }); + + it('clears a pending request after remove, once the goal has stopped', () => { + expect(goalClearsBlockingAfterSend('remove')).toBe(true); + }); + + it('clears a pending request before resume, which the CLI rejects while blocked', () => { + expect(goalClearsBlockingAfterSend('resume')).toBe(false); + }); + + it('clears a pending request before edit, which the CLI rejects while blocked', () => { + expect(goalClearsBlockingAfterSend('edit')).toBe(false); + }); +}); + +describe('goalCommandArguments', () => { + it('sends the objective for edit', () => { + expect(goalCommandArguments('edit', 'Ship the release')).toBe('Ship the release'); + }); + + it('maps pause to the CLI pause argument', () => { + expect(goalCommandArguments('pause', 'Ship the release')).toBe('pause'); + }); + + it('maps resume to the CLI resume argument', () => { + expect(goalCommandArguments('resume', 'Ship the release')).toBe('resume'); + }); + + it('maps remove to the CLI clear argument', () => { + expect(goalCommandArguments('remove', 'Ship the release')).toBe('clear'); + }); +}); + +describe('selectVisibleGoal', () => { + const paused = goal('paused'); + + it('returns the goal for a live session that reports one', () => { + const info: SessionInfo = { id: 'ses_live', goal: paused }; + expect(selectVisibleGoal(info, false)).toEqual(paused); + }); + + it('hides the goal for a read-only session even when its snapshot carries one', () => { + const info: SessionInfo = { id: 'ses_read_only', goal: paused }; + expect(selectVisibleGoal(info, true)).toBeNull(); + }); + + it('returns null when the session has no goal', () => { + expect(selectVisibleGoal({ id: 'ses_plain' }, false)).toBeNull(); + expect(selectVisibleGoal(null, false)).toBeNull(); + }); +}); diff --git a/apps/mobile/src/components/agents/session-goal-actions.ts b/apps/mobile/src/components/agents/session-goal-actions.ts new file mode 100644 index 0000000000..c43d10e6db --- /dev/null +++ b/apps/mobile/src/components/agents/session-goal-actions.ts @@ -0,0 +1,68 @@ +import { type SessionGoal, type SessionInfo } from '@kilocode/cloud-agent-sdk'; + +/** Goal controls offered by tapping the fixed goal section. */ +export type GoalAction = 'edit' | 'pause' | 'resume' | 'remove'; + +/** + * The fixed goal row is an action surface: tapping it sends `/goal`. A + * read-only session has no CLI owner to accept that command, yet its + * historical snapshot can still carry `kilo.goal` metadata (the page + * transport projects it). Hide the row for read-only sessions so the + * metadata never turns into controls the session cannot support. + */ +export function selectVisibleGoal( + info: SessionInfo | null, + isReadOnly: boolean +): SessionGoal | null { + if (isReadOnly) { + return null; + } + return info?.goal ?? null; +} + +/** + * The CLI accepts `/goal pause` and `/goal clear` while a question or + * permission is pending, but the pending request then blocks every later + * resume. The app clears it *after* the goal has stopped: the CLI records a + * rejected request as a blocked goal only while the goal is still running, so + * clearing after pause/clear leaves the goal paused instead of blocked. + * + * `/goal resume` and `/goal ` are rejected while a request is + * pending, so those controls clear it *before* they send. + */ +export function goalClearsBlockingAfterSend(action: GoalAction): boolean { + return action === 'pause' || action === 'remove'; +} + +/** + * Mirrors the CLI goal actions: Pause is offered only while active. Resume is + * offered for every non-active state — the CLI continues a paused goal, resumes + * a blocked goal once its blocker is resolved, and restarts a complete goal. + * Edit and Remove are always available. + */ +export function resolveGoalActions(goal: SessionGoal): GoalAction[] { + const actions: GoalAction[] = ['edit']; + if (goal.status === 'active') { + actions.push('pause'); + } else { + actions.push('resume'); + } + actions.push('remove'); + return actions; +} + +/** Maps a goal control to the `/goal` command argument the CLI expects. */ +export function goalCommandArguments(action: GoalAction, objective: string): string { + // The `edit` action carries its own objective; the rest map to fixed CLI + // subcommands. `clear` is the remove action's wire argument. + if (action === 'edit') { + return objective; + } + if (action === 'pause') { + return 'pause'; + } + if (action === 'resume') { + return 'resume'; + } + return 'clear'; +} diff --git a/apps/mobile/src/components/agents/session-goal-section.tsx b/apps/mobile/src/components/agents/session-goal-section.tsx new file mode 100644 index 0000000000..0d898729d6 --- /dev/null +++ b/apps/mobile/src/components/agents/session-goal-section.tsx @@ -0,0 +1,59 @@ +import { type SessionGoal, type SessionGoalStatus } from '@kilocode/cloud-agent-sdk'; +import { useTranslation } from 'react-i18next'; +import { Pressable, View } from 'react-native'; + +import { CircleDot } from '@/components/ui/icons'; +import { Text } from '@/components/ui/text'; +import { useThemeColors } from '@/lib/hooks/use-theme-colors'; +import { cn } from '@/lib/utils'; + +const STATUS_LABEL_KEY = { + active: 'agentChat.goal.statusActive', + paused: 'agentChat.goal.statusPaused', + complete: 'agentChat.goal.statusComplete', + blocked: 'agentChat.goal.statusBlocked', +} as const satisfies Record; + +type SessionGoalSectionProps = { + goal: SessionGoal; + onPress: () => void; +}; + +/** + * Fixed goal row shown under the session header. It lives outside the + * transcript list, so it stays put while the transcript scrolls. The + * `min-h-*` reserves the row height across every status, and the optional + * `reason` is the only part that can add a line. + */ +export function SessionGoalSection({ goal, onPress }: Readonly) { + const colors = useThemeColors(); + const { t } = useTranslation(); + const isActive = goal.status === 'active'; + const status = t(STATUS_LABEL_KEY[goal.status]); + + return ( + + + + + + + {status} + + + {goal.text} + + {goal.reason ? ( + + {goal.reason} + + ) : null} + + + ); +} diff --git a/apps/mobile/src/components/agents/use-interaction-handlers.ts b/apps/mobile/src/components/agents/use-interaction-handlers.ts index 16c684c485..34632af9c5 100644 --- a/apps/mobile/src/components/agents/use-interaction-handlers.ts +++ b/apps/mobile/src/components/agents/use-interaction-handlers.ts @@ -47,9 +47,9 @@ export function useInteractionHandlers({ } | null>(null); const handleAnswerQuestion = useCallback( - async (answers: string[][]) => { + async (answers: string[][]): Promise => { if (!activeQuestion) { - return; + return false; } const requestId = activeQuestion.requestId; setQuestionSubmissionError(null); @@ -58,11 +58,13 @@ export function useInteractionHandlers({ await manager.answerQuestion(requestId, answers); ackSessionAttention(kiloSessionId); captureEvent(QUESTION_ANSWERED_EVENT, { surface, skipped: false }); + return true; } catch (error) { const submissionError = classifyBlockingSubmissionError(error, 'question', 'answer'); setQuestionSubmissionError({ requestId, error: submissionError }); announceForA11y(submissionError.message); toast.error(submissionError.message); + return false; } finally { // Guarded clear. The head can advance mid-flight, the user can submit the // next request, and this late `finally` must not drop that newer spinner. @@ -72,9 +74,9 @@ export function useInteractionHandlers({ [manager, kiloSessionId, activeQuestion, surface] ); - const handleRejectQuestion = useCallback(async () => { + const handleRejectQuestion = useCallback(async (): Promise => { if (!activeQuestion) { - return; + return false; } const requestId = activeQuestion.requestId; setQuestionSubmissionError(null); @@ -83,20 +85,22 @@ export function useInteractionHandlers({ await manager.rejectQuestion(requestId); ackSessionAttention(kiloSessionId); captureEvent(QUESTION_ANSWERED_EVENT, { surface, skipped: true }); + return true; } catch (error) { const submissionError = classifyBlockingSubmissionError(error, 'question', 'reject'); setQuestionSubmissionError({ requestId, error: submissionError }); announceForA11y(submissionError.message); toast.error(submissionError.message); + return false; } finally { setAnsweringRequestId(current => (current === requestId ? null : current)); } }, [manager, kiloSessionId, activeQuestion, surface]); const handleRespondToPermission = useCallback( - async (response: 'once' | 'always' | 'reject') => { + async (response: 'once' | 'always' | 'reject'): Promise => { if (!activePermission) { - return; + return false; } const requestId = activePermission.requestId; setPermissionSubmissionError(null); @@ -105,6 +109,7 @@ export function useInteractionHandlers({ await manager.respondToPermission(requestId, response); ackSessionAttention(kiloSessionId); captureEvent(PERMISSION_RESPONDED_EVENT, { surface, response }); + return true; } catch (error) { const submissionError = classifyBlockingSubmissionError(error, 'permission', 'respond'); setPermissionSubmissionError({ @@ -113,6 +118,7 @@ export function useInteractionHandlers({ }); announceForA11y(submissionError.message); toast.error(submissionError.message); + return false; } finally { setRespondingRequestId(current => (current === requestId ? null : current)); } diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index d3e6ba9dee..8c026e8713 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -1996,6 +1996,22 @@ "connectionLost": "Verbinding verloor", "retryConnection": "Probeer weer koppel" }, + "goal": { + "title": "Doel", + "statusActive": "Aktiewe doel", + "statusPaused": "Doel opgeskort", + "statusComplete": "Doel voltooi", + "statusBlocked": "Doel geblokkeer", + "edit": "Doel wysig", + "remove": "Doel verwyder", + "pause": "Doel onderbreek", + "resume": "Doel hervat", + "editPlaceholder": "Beskryf die doel", + "removeConfirmTitle": "Doel verwyder?", + "removeConfirmMessage": "Die CLI verwyder hierdie sessie se doel.", + "updateFailed": "Kon nie die doel bywerk nie.", + "sectionAccessibility": "{{status}}. Doel: {{text}}" + }, "newSession": { "connectGithubDescription": "Koppel GitHub in jou blaaier en kom dan hierheen terug om 'n bewaarplek te kies.", "openGithub": "Maak GitHub oop", diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index d91dfc0bc8..7c8061a083 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -1996,6 +1996,22 @@ "connectionLost": "ግንኙነቱ ተቋርጧል", "retryConnection": "እንደገና ለመገናኘት ይሞክሩ" }, + "goal": { + "title": "ግብ", + "statusActive": "ንቁ ግብ", + "statusPaused": "የቆመ ግብ", + "statusComplete": "ግብ ተጠናቅቋል", + "statusBlocked": "ግብ ተዘግቷል", + "edit": "ግብን አርትዕ", + "remove": "ግብን አስወግድ", + "pause": "ግብን አቁም", + "resume": "ግብን ቀጥል", + "editPlaceholder": "ግቡን ይግለጹ", + "removeConfirmTitle": "ግብ ይወገድ?", + "removeConfirmMessage": "CLI የዚህን ክፍለ ጊዜ ግብ ያጸዳል።", + "updateFailed": "ግቡን ማዘመን አልተቻለም።", + "sectionAccessibility": "{{status}}። ግብ፦ {{text}}" + }, "newSession": { "connectGithubDescription": "GitHubን በአሳሽዎ ውስጥ ያገናኙ፣ ከዚያ ማከማቻ ለመምረጥ ወደዚህ ይመለሱ።", "openGithub": "GitHubን ይክፈቱ", diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index eeca6cc4bd..7941007d16 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -1598,6 +1598,22 @@ "connectionLost": "انقطع الاتصال", "retryConnection": "إعادة محاولة الاتصال" }, + "goal": { + "title": "الهدف", + "statusActive": "هدف نشط", + "statusPaused": "هدف متوقف مؤقتًا", + "statusComplete": "اكتمل الهدف", + "statusBlocked": "الهدف محظور", + "edit": "تعديل الهدف", + "remove": "إزالة الهدف", + "pause": "إيقاف الهدف مؤقتًا", + "resume": "استئناف الهدف", + "editPlaceholder": "صف الهدف", + "removeConfirmTitle": "إزالة الهدف؟", + "removeConfirmMessage": "يمسح CLI هدف هذه الجلسة.", + "updateFailed": "تعذّر تحديث الهدف.", + "sectionAccessibility": "{{status}}. الهدف: {{text}}" + }, "newSession": { "connectGithubDescription": "اربط حسابك على GitHub من المتصفح، ثم عد إلى هنا لاختيار مستودع.", "openGithub": "فتح GitHub", diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index 0284c8ab60..c3c84331d0 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -1996,6 +1996,22 @@ "connectionLost": "Bağlantı kəsildi", "retryConnection": "Yenidən qoşul" }, + "goal": { + "title": "Məqsəd", + "statusActive": "Aktiv məqsəd", + "statusPaused": "Dayandırılmış məqsəd", + "statusComplete": "Məqsəd tamamlandı", + "statusBlocked": "Məqsəd bloklandı", + "edit": "Məqsədi redaktə et", + "remove": "Məqsədi sil", + "pause": "Məqsədi dayandır", + "resume": "Məqsədə davam et", + "editPlaceholder": "Məqsədi təsvir edin", + "removeConfirmTitle": "Məqsəd silinsin?", + "removeConfirmMessage": "CLI bu sessiyanın məqsədini təmizləyir.", + "updateFailed": "Məqsəd yenilənə bilmədi.", + "sectionAccessibility": "{{status}}. Məqsəd: {{text}}" + }, "newSession": { "connectGithubDescription": "GitHub hesabını brauzerdə qoş, sonra repozitoriya seçmək üçün buraya qayıt.", "openGithub": "GitHub-u aç", diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index 01b8346a2d..a12d723345 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -2028,6 +2028,22 @@ "connectionLost": "Сувязь страчана", "retryConnection": "Паўтарыць падключэнне" }, + "goal": { + "title": "Мэта", + "statusActive": "Актыўная мэта", + "statusPaused": "Прыпыненая мэта", + "statusComplete": "Мэта выканана", + "statusBlocked": "Мэта заблакавана", + "edit": "Рэдагаваць мэту", + "remove": "Выдаліць мэту", + "pause": "Прыпыніць мэту", + "resume": "Працягнуць мэту", + "editPlaceholder": "Апішыце мэту", + "removeConfirmTitle": "Выдаліць мэту?", + "removeConfirmMessage": "CLI ачышчае мэту гэтага сеансу.", + "updateFailed": "Не ўдалося абнавіць мэту.", + "sectionAccessibility": "{{status}}. Мэта: {{text}}" + }, "newSession": { "connectGithubDescription": "Падключыце GitHub у браўзеры, потым вярніцеся сюды, каб выбраць рэпазіторый.", "openGithub": "Адкрыць GitHub", diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index 1bcde3736e..c646b66b9a 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -1996,6 +1996,22 @@ "connectionLost": "Прекъсната връзка", "retryConnection": "Опитай да се свържеш отново" }, + "goal": { + "title": "Цел", + "statusActive": "Активна цел", + "statusPaused": "Цел на пауза", + "statusComplete": "Целта е изпълнена", + "statusBlocked": "Целта е блокирана", + "edit": "Редактиране на целта", + "remove": "Премахване на целта", + "pause": "Пауза на целта", + "resume": "Продължаване на целта", + "editPlaceholder": "Опишете целта", + "removeConfirmTitle": "Премахване на целта?", + "removeConfirmMessage": "CLI изчиства целта на тази сесия.", + "updateFailed": "Целта не можа да се обнови.", + "sectionAccessibility": "{{status}}. Цел: {{text}}" + }, "newSession": { "connectGithubDescription": "Свържи GitHub в браузъра си, след което се върни тук, за да избереш хранилище.", "openGithub": "Отвори GitHub", diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index 5679c4ca18..88f4005d92 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -1996,6 +1996,22 @@ "connectionLost": "সংযোগ বিচ্ছিন্ন", "retryConnection": "আবার সংযোগের চেষ্টা করুন" }, + "goal": { + "title": "লক্ষ্য", + "statusActive": "সক্রিয় লক্ষ্য", + "statusPaused": "বিরত থাকা লক্ষ্য", + "statusComplete": "লক্ষ্য সম্পন্ন", + "statusBlocked": "লক্ষ্য অবরুদ্ধ", + "edit": "লক্ষ্য সম্পাদনা করুন", + "remove": "লক্ষ্য সরান", + "pause": "লক্ষ্য বিরত করুন", + "resume": "লক্ষ্য পুনরায় শুরু করুন", + "editPlaceholder": "লক্ষ্য বর্ণনা করুন", + "removeConfirmTitle": "লক্ষ্য সরাবেন?", + "removeConfirmMessage": "CLI এই সেশনের লক্ষ্য মুছে দেবে।", + "updateFailed": "লক্ষ্য আপডেট করা যায়নি।", + "sectionAccessibility": "{{status}}। লক্ষ্য: {{text}}" + }, "newSession": { "connectGithubDescription": "আপনার ব্রাউজারে GitHub সংযুক্ত করুন, তারপর একটি রিপোজিটরি বেছে নিতে এখানে ফিরে আসুন।", "openGithub": "GitHub খুলুন", diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index 7ff8a51470..0ec8e10b23 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -2012,6 +2012,22 @@ "connectionLost": "Veza izgubljena", "retryConnection": "Ponovo se poveži" }, + "goal": { + "title": "Cilj", + "statusActive": "Aktivan cilj", + "statusPaused": "Pauziran cilj", + "statusComplete": "Cilj završen", + "statusBlocked": "Cilj blokiran", + "edit": "Uredi cilj", + "remove": "Ukloni cilj", + "pause": "Pauziraj cilj", + "resume": "Nastavi cilj", + "editPlaceholder": "Opišite cilj", + "removeConfirmTitle": "Ukloniti cilj?", + "removeConfirmMessage": "CLI briše cilj ove sesije.", + "updateFailed": "Nije bilo moguće ažurirati cilj.", + "sectionAccessibility": "{{status}}. Cilj: {{text}}" + }, "newSession": { "connectGithubDescription": "Poveži GitHub u pregledniku, zatim se vrati ovdje i odaberi repozitorij.", "openGithub": "Otvori GitHub", diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index b06d675ba5..888cddefa7 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -2012,6 +2012,22 @@ "connectionLost": "Connexió perduda", "retryConnection": "Torna a provar la connexió" }, + "goal": { + "title": "Objectiu", + "statusActive": "Objectiu actiu", + "statusPaused": "Objectiu en pausa", + "statusComplete": "Objectiu completat", + "statusBlocked": "Objectiu bloquejat", + "edit": "Edita l'objectiu", + "remove": "Elimina l'objectiu", + "pause": "Posa en pausa l'objectiu", + "resume": "Reprèn l'objectiu", + "editPlaceholder": "Descriu l'objectiu", + "removeConfirmTitle": "Eliminar l'objectiu?", + "removeConfirmMessage": "El CLI esborra l'objectiu d'aquesta sessió.", + "updateFailed": "No s'ha pogut actualitzar l'objectiu.", + "sectionAccessibility": "{{status}}. Objectiu: {{text}}" + }, "newSession": { "connectGithubDescription": "Connecta GitHub al navegador i torna aquí per triar un repositori.", "openGithub": "Obre GitHub", diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index 6cf8060853..ce1167ca2e 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -1996,6 +1996,22 @@ "connectionLost": "پەیوەندییەکە پچڕا", "retryConnection": "دووبارە هەوڵدان بۆ پەیوەندیکردن" }, + "goal": { + "title": "ئامانج", + "statusActive": "ئامانجی چالاک", + "statusPaused": "ئامانجی وەستاو", + "statusComplete": "ئامانج تەواو بوو", + "statusBlocked": "ئامانج بلۆک کراوە", + "edit": "دەستکاری ئامانج بکە", + "remove": "ئامانج لابەرە", + "pause": "ئامانج ڕابگرە", + "resume": "ئامانج بەردەوام بکە", + "editPlaceholder": "ئامانجەکە باس بکە", + "removeConfirmTitle": "ئامانج لاببرێت؟", + "removeConfirmMessage": "CLI ئامانجی ئەم دانیشتنە دەسڕێتەوە.", + "updateFailed": "نەتوانرا ئامانج نوێ بکرێتەوە.", + "sectionAccessibility": "{{status}}. ئامانج: {{text}}" + }, "newSession": { "connectGithubDescription": "لە وێبگەڕەکەتدا هەژماری GitHub ببەستەرەوە، پاشان بگەڕێوە ئێرە و کۆگایەک هەڵبژێرە.", "openGithub": "کردنەوەی GitHub", diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 81f2cbdc1f..329da57390 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -2028,6 +2028,22 @@ "connectionLost": "Připojení přerušeno", "retryConnection": "Znovu se připojit" }, + "goal": { + "title": "Cíl", + "statusActive": "Aktivní cíl", + "statusPaused": "Pozastavený cíl", + "statusComplete": "Cíl dokončen", + "statusBlocked": "Cíl zablokován", + "edit": "Upravit cíl", + "remove": "Odebrat cíl", + "pause": "Pozastavit cíl", + "resume": "Obnovit cíl", + "editPlaceholder": "Popište cíl", + "removeConfirmTitle": "Odebrat cíl?", + "removeConfirmMessage": "CLI vymaže cíl této relace.", + "updateFailed": "Cíl se nepodařilo aktualizovat.", + "sectionAccessibility": "{{status}}. Cíl: {{text}}" + }, "newSession": { "connectGithubDescription": "Připojte GitHub v prohlížeči. Pak se sem vraťte a vyberte repozitář.", "openGithub": "Otevřít GitHub", diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index ce90952027..f51d9a9857 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -2060,6 +2060,22 @@ "connectionLost": "Cysylltiad wedi'i golli", "retryConnection": "Ceisio cysylltu eto" }, + "goal": { + "title": "Nod", + "statusActive": "Nod gweithredol", + "statusPaused": "Nod wedi'i phasio", + "statusComplete": "Nod wedi'i chwblhau", + "statusBlocked": "Nod wedi'i blocio", + "edit": "Golygu'r nod", + "remove": "Tynnu'r nod", + "pause": "Oedi'r nod", + "resume": "Ailddechrau'r nod", + "editPlaceholder": "Disgrifiwch y nod", + "removeConfirmTitle": "Tynnu'r nod?", + "removeConfirmMessage": "Mae'r CLI yn clirio nod y sesiwn hon.", + "updateFailed": "Methwyd diweddaru'r nod.", + "sectionAccessibility": "{{status}}. Nod: {{text}}" + }, "newSession": { "connectGithubDescription": "Cysylltwch â GitHub yn eich porwr, yna dewch yn ôl yma i ddewis ystorfa.", "openGithub": "Agor GitHub", diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index 67bd86fa4b..c830829e94 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -1996,6 +1996,22 @@ "connectionLost": "Forbindelsen er afbrudt", "retryConnection": "Prøv at oprette forbindelse igen" }, + "goal": { + "title": "Mål", + "statusActive": "Aktivt mål", + "statusPaused": "Mål sat på pause", + "statusComplete": "Mål fuldført", + "statusBlocked": "Mål blokeret", + "edit": "Rediger mål", + "remove": "Fjern mål", + "pause": "Sæt mål på pause", + "resume": "Genoptag mål", + "editPlaceholder": "Beskriv målet", + "removeConfirmTitle": "Fjern mål?", + "removeConfirmMessage": "CLI rydder dette sessions mål.", + "updateFailed": "Målet kunne ikke opdateres.", + "sectionAccessibility": "{{status}}. Mål: {{text}}" + }, "newSession": { "connectGithubDescription": "Tilknyt GitHub i din browser og vend tilbage hertil for at vælge et kodelager.", "openGithub": "Åbn GitHub", diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index 41e197c559..834b4714cf 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -1562,6 +1562,22 @@ "connectionLost": "Verbindung unterbrochen", "retryConnection": "Erneut verbinden" }, + "goal": { + "title": "Ziel", + "statusActive": "Aktives Ziel", + "statusPaused": "Pausiertes Ziel", + "statusComplete": "Ziel abgeschlossen", + "statusBlocked": "Ziel blockiert", + "edit": "Ziel bearbeiten", + "remove": "Ziel entfernen", + "pause": "Ziel pausieren", + "resume": "Ziel fortsetzen", + "editPlaceholder": "Ziel beschreiben", + "removeConfirmTitle": "Ziel entfernen?", + "removeConfirmMessage": "Die CLI löscht das Ziel dieser Sitzung.", + "updateFailed": "Das Ziel konnte nicht aktualisiert werden.", + "sectionAccessibility": "{{status}}. Ziel: {{text}}" + }, "newSession": { "connectGithubDescription": "Verbinde GitHub in deinem Browser und kehre dann hierher zurück, um ein Repository auszuwählen.", "openGithub": "GitHub öffnen", diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index b000606229..7decdae747 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -1996,6 +1996,22 @@ "connectionLost": "Χάθηκε η σύνδεση", "retryConnection": "Επανάληψη σύνδεσης" }, + "goal": { + "title": "Στόχος", + "statusActive": "Ενεργός στόχος", + "statusPaused": "Στόχος σε παύση", + "statusComplete": "Ο στόχος ολοκληρώθηκε", + "statusBlocked": "Ο στόχος μπλοκαρίστηκε", + "edit": "Επεξεργασία στόχου", + "remove": "Αφαίρεση στόχου", + "pause": "Παύση στόχου", + "resume": "Συνέχιση στόχου", + "editPlaceholder": "Περιγράψτε τον στόχο", + "removeConfirmTitle": "Αφαίρεση στόχου;", + "removeConfirmMessage": "Το CLI καθαρίζει τον στόχο αυτής της συνεδρίας.", + "updateFailed": "Δεν ήταν δυνατή η ενημέρωση του στόχου.", + "sectionAccessibility": "{{status}}. Στόχος: {{text}}" + }, "newSession": { "connectGithubDescription": "Σύνδεσε το GitHub από το πρόγραμμα περιήγησης και μετά επέστρεψε εδώ για να επιλέξεις ένα αποθετήριο.", "openGithub": "Άνοιγμα GitHub", diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 638b2fe493..395fbabf82 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -2004,6 +2004,22 @@ "connectionLost": "Connection lost", "retryConnection": "Retry connection" }, + "goal": { + "title": "Goal", + "statusActive": "Active goal", + "statusPaused": "Paused goal", + "statusComplete": "Goal complete", + "statusBlocked": "Goal blocked", + "edit": "Edit goal", + "remove": "Remove goal", + "pause": "Pause goal", + "resume": "Resume goal", + "editPlaceholder": "Describe the goal", + "removeConfirmTitle": "Remove goal?", + "removeConfirmMessage": "The CLI clears this session's goal.", + "updateFailed": "Could not update the goal.", + "sectionAccessibility": "{{status}}. Goal: {{text}}" + }, "newSession": { "recentlyUsed": "Recently used", "couldNotLoadGithubRepositories": "Couldn't load GitHub repositories", diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index cafd283c42..b53ab003a3 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -1571,6 +1571,22 @@ "connectionLost": "Conexión perdida", "retryConnection": "Reintentar la conexión" }, + "goal": { + "title": "Objetivo", + "statusActive": "Objetivo activo", + "statusPaused": "Objetivo en pausa", + "statusComplete": "Objetivo completado", + "statusBlocked": "Objetivo bloqueado", + "edit": "Editar objetivo", + "remove": "Eliminar objetivo", + "pause": "Pausar objetivo", + "resume": "Reanudar objetivo", + "editPlaceholder": "Describe el objetivo", + "removeConfirmTitle": "¿Eliminar objetivo?", + "removeConfirmMessage": "El CLI borra el objetivo de esta sesión.", + "updateFailed": "No se pudo actualizar el objetivo.", + "sectionAccessibility": "{{status}}. Objetivo: {{text}}" + }, "newSession": { "connectGithubDescription": "Conecta GitHub en tu navegador y vuelve aquí para elegir un repositorio.", "openGithub": "Abrir GitHub", diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index f43bd9ad18..5360826dc5 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -1996,6 +1996,22 @@ "connectionLost": "Ühendus katkes", "retryConnection": "Proovi uuesti ühenduda" }, + "goal": { + "title": "Eesmärk", + "statusActive": "Aktiivne eesmärk", + "statusPaused": "Peatatud eesmärk", + "statusComplete": "Eesmärk täidetud", + "statusBlocked": "Eesmärk blokeeritud", + "edit": "Muuda eesmärki", + "remove": "Eemalda eesmärk", + "pause": "Peata eesmärk", + "resume": "Jätka eesmärki", + "editPlaceholder": "Kirjelda eesmärki", + "removeConfirmTitle": "Eemaldada eesmärk?", + "removeConfirmMessage": "CLI tühjendab selle seansi eesmärgi.", + "updateFailed": "Eesmärki ei saanud värskendada.", + "sectionAccessibility": "{{status}}. Eesmärk: {{text}}" + }, "newSession": { "connectGithubDescription": "Ühenda GitHub brauseris ja naase siia, et valida hoidla.", "openGithub": "Ava GitHub", diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index eabdc8c73f..ab5362f205 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -1996,6 +1996,22 @@ "connectionLost": "Konexioa galdu da", "retryConnection": "Saiatu berriro konektatzen" }, + "goal": { + "title": "Helburua", + "statusActive": "Helburu aktiboa", + "statusPaused": "Gelditutako helburua", + "statusComplete": "Helburua osatuta", + "statusBlocked": "Helburua blokeatuta", + "edit": "Editatu helburua", + "remove": "Kendu helburua", + "pause": "Eten helburua", + "resume": "Berrekin helburuari", + "editPlaceholder": "Deskribatu helburua", + "removeConfirmTitle": "Helburua kendu?", + "removeConfirmMessage": "CLI-k saio honen helburua ezabatzen du.", + "updateFailed": "Ezin izan da helburua eguneratu.", + "sectionAccessibility": "{{status}}. Helburua: {{text}}" + }, "newSession": { "connectGithubDescription": "Konektatu GitHub zure nabigatzailean, eta gero itzuli hona biltegi bat aukeratzeko.", "openGithub": "Ireki GitHub", diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index 1c892d3aa3..81802f1eb1 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -1996,6 +1996,22 @@ "connectionLost": "اتصال قطع شد", "retryConnection": "تلاش دوباره برای اتصال" }, + "goal": { + "title": "هدف", + "statusActive": "هدف فعال", + "statusPaused": "هدف متوقفشده", + "statusComplete": "هدف تکمیل شد", + "statusBlocked": "هدف مسدود شد", + "edit": "ویرایش هدف", + "remove": "حذف هدف", + "pause": "توقف هدف", + "resume": "ادامه هدف", + "editPlaceholder": "هدف را توصیف کنید", + "removeConfirmTitle": "هدف حذف شود؟", + "removeConfirmMessage": "CLI هدف این جلسه را پاک میکند.", + "updateFailed": "هدف بهروزرسانی نشد.", + "sectionAccessibility": "{{status}}. هدف: {{text}}" + }, "newSession": { "connectGithubDescription": "حساب GitHub خود را از طریق مرورگر متصل کنید، سپس برای انتخاب مخزن به اینجا برگردید.", "openGithub": "باز کردن GitHub", diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index bf759dbad0..2ae7ee8bc7 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -1996,6 +1996,22 @@ "connectionLost": "Yhteys katkesi", "retryConnection": "Yhdistä uudelleen" }, + "goal": { + "title": "Tavoite", + "statusActive": "Aktiivinen tavoite", + "statusPaused": "Keskeytetty tavoite", + "statusComplete": "Tavoite valmis", + "statusBlocked": "Tavoite estetty", + "edit": "Muokkaa tavoitetta", + "remove": "Poista tavoite", + "pause": "Keskeytä tavoite", + "resume": "Jatka tavoitetta", + "editPlaceholder": "Kuvaile tavoite", + "removeConfirmTitle": "Poistetaanko tavoite?", + "removeConfirmMessage": "CLI tyhjentää tämän istunnon tavoitteen.", + "updateFailed": "Tavoitetta ei voitu päivittää.", + "sectionAccessibility": "{{status}}. Tavoite: {{text}}" + }, "newSession": { "connectGithubDescription": "Yhdistä GitHub selaimessa ja palaa sitten tänne valitsemaan repositorio.", "openGithub": "Avaa GitHub", diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index 9a7674b10c..f285394039 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -1996,6 +1996,22 @@ "connectionLost": "Nawala ang koneksyon", "retryConnection": "Subukang kumonekta muli" }, + "goal": { + "title": "Layunin", + "statusActive": "Aktibong layunin", + "statusPaused": "Nakapausang layunin", + "statusComplete": "Kumpleto ang layunin", + "statusBlocked": "Naharang ang layunin", + "edit": "I-edit ang layunin", + "remove": "Alisin ang layunin", + "pause": "I-pause ang layunin", + "resume": "Ipagpatuloy ang layunin", + "editPlaceholder": "Ilarawan ang layunin", + "removeConfirmTitle": "Alisin ang layunin?", + "removeConfirmMessage": "Kinaklaro ng CLI ang layunin ng session na ito.", + "updateFailed": "Hindi na-update ang layunin.", + "sectionAccessibility": "{{status}}. Layunin: {{text}}" + }, "newSession": { "connectGithubDescription": "Ikonekta ang GitHub sa iyong browser, saka bumalik dito para pumili ng imbakan ng code.", "openGithub": "Buksan ang GitHub", diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index 1156a4138a..1a7ec37119 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -1477,6 +1477,22 @@ "connectionLost": "Connexion perdue", "retryConnection": "Réessayer de se connecter" }, + "goal": { + "title": "Objectif", + "statusActive": "Objectif actif", + "statusPaused": "Objectif en pause", + "statusComplete": "Objectif terminé", + "statusBlocked": "Objectif bloqué", + "edit": "Modifier l'objectif", + "remove": "Supprimer l'objectif", + "pause": "Mettre l'objectif en pause", + "resume": "Reprendre l'objectif", + "editPlaceholder": "Décrivez l'objectif", + "removeConfirmTitle": "Supprimer l'objectif ?", + "removeConfirmMessage": "Le CLI efface l'objectif de cette session.", + "updateFailed": "Impossible de mettre à jour l'objectif.", + "sectionAccessibility": "{{status}}. Objectif : {{text}}" + }, "newSession": { "connectGithubDescription": "Connectez votre compte GitHub dans votre navigateur, puis revenez ici pour choisir un dépôt.", "openGithub": "Ouvrir GitHub", diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index 78621869df..d73a6ae15e 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -2044,6 +2044,22 @@ "connectionLost": "Cailleadh an nasc", "retryConnection": "Bain triail eile as nascadh" }, + "goal": { + "title": "Sprioc", + "statusActive": "Sprioc ghníomhach", + "statusPaused": "Sprioc curtha ar sos", + "statusComplete": "Sprioc curtha i gcrích", + "statusBlocked": "Sprioc blocáilte", + "edit": "Cuir an sprioc in eagar", + "remove": "Bain an sprioc", + "pause": "Cuir an sprioc ar sos", + "resume": "Atosaigh an sprioc", + "editPlaceholder": "Déan cur síos ar an sprioc", + "removeConfirmTitle": "Bain an sprioc?", + "removeConfirmMessage": "Glanann an CLI sprioc an tseisiúin seo.", + "updateFailed": "Níorbh fhéidir an sprioc a nuashonrú.", + "sectionAccessibility": "{{status}}. Sprioc: {{text}}" + }, "newSession": { "connectGithubDescription": "Ceangail GitHub i do bhrabhsálaí, ansin fill anseo chun stór a roghnú.", "openGithub": "Oscail GitHub", diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index bc3ad174a2..f9e7bd61ef 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -1996,6 +1996,22 @@ "connectionLost": "Perdeuse a conexión", "retryConnection": "Volver tentar a conexión" }, + "goal": { + "title": "Obxectivo", + "statusActive": "Obxectivo activo", + "statusPaused": "Obxectivo en pausa", + "statusComplete": "Obxectivo completado", + "statusBlocked": "Obxectivo bloqueado", + "edit": "Editar o obxectivo", + "remove": "Eliminar o obxectivo", + "pause": "Pausar o obxectivo", + "resume": "Retomar o obxectivo", + "editPlaceholder": "Describe o obxectivo", + "removeConfirmTitle": "Eliminar o obxectivo?", + "removeConfirmMessage": "O CLI borra o obxectivo desta sesión.", + "updateFailed": "Non se puido actualizar o obxectivo.", + "sectionAccessibility": "{{status}}. Obxectivo: {{text}}" + }, "newSession": { "connectGithubDescription": "Conecta GitHub no teu navegador e despois volve aquí para escoller un repositorio.", "openGithub": "Abrir GitHub", diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index b5a8d3cb79..cc0fd0fd04 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -1996,6 +1996,22 @@ "connectionLost": "કનેક્શન તૂટી ગયું", "retryConnection": "કનેક્ટ કરવા ફરી પ્રયાસ કરો" }, + "goal": { + "title": "ધ્યેય", + "statusActive": "સક્રિય ધ્યેય", + "statusPaused": "થોભેલું ધ્યેય", + "statusComplete": "ધ્યેય પૂર્ણ", + "statusBlocked": "ધ્યેય અવરોધિત", + "edit": "ધ્યેય સંપાદિત કરો", + "remove": "ધ્યેય દૂર કરો", + "pause": "ધ્યેય થોભાવો", + "resume": "ધ્યેય ફરી શરૂ કરો", + "editPlaceholder": "ધ્યેયનું વર્ણન કરો", + "removeConfirmTitle": "ધ્યેય દૂર કરવું?", + "removeConfirmMessage": "CLI આ સત્રનું ધ્યેય સાફ કરે છે.", + "updateFailed": "ધ્યેય અપડેટ કરી શકાયું નહીં.", + "sectionAccessibility": "{{status}}. ધ્યેય: {{text}}" + }, "newSession": { "connectGithubDescription": "તમારા બ્રાઉઝરમાં GitHub કનેક્ટ કરો, પછી રિપોઝિટરી પસંદ કરવા અહીં પાછા આવો.", "openGithub": "GitHub ખોલો", diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index f1993b3044..99b1989f94 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -1996,6 +1996,22 @@ "connectionLost": "Haɗi ya katse", "retryConnection": "Sake gwada haɗi" }, + "goal": { + "title": "Manufa", + "statusActive": "Manufa mai aiki", + "statusPaused": "Manufar da aka dakatar", + "statusComplete": "An kammala manufa", + "statusBlocked": "An toshe manufa", + "edit": "Gyara manufa", + "remove": "Cire manufa", + "pause": "Dakatar da manufa", + "resume": "Ci gaba da manufa", + "editPlaceholder": "Bayyana manufa", + "removeConfirmTitle": "Cire manufa?", + "removeConfirmMessage": "CLI yana share manufar wannan zaman.", + "updateFailed": "Ba a iya sabunta manufar ba.", + "sectionAccessibility": "{{status}}. Manufa: {{text}}" + }, "newSession": { "connectGithubDescription": "Haɗa GitHub a burauzarka, sannan ka dawo nan don zaɓar ma'ajiya.", "openGithub": "Buɗe GitHub", diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index 68f3c687ce..c8eb50ba48 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -1571,6 +1571,22 @@ "connectionLost": "החיבור נותק", "retryConnection": "ניסיון התחברות נוסף" }, + "goal": { + "title": "מטרה", + "statusActive": "מטרה פעילה", + "statusPaused": "מטרה מושהית", + "statusComplete": "המטרה הושלמה", + "statusBlocked": "המטרה חסומה", + "edit": "ערוך מטרה", + "remove": "הסר מטרה", + "pause": "השהה מטרה", + "resume": "המשך מטרה", + "editPlaceholder": "תאר את המטרה", + "removeConfirmTitle": "להסיר את המטרה?", + "removeConfirmMessage": "ה-CLI מנקה את מטרת ההפעלה הזו.", + "updateFailed": "לא ניתן היה לעדכן את המטרה.", + "sectionAccessibility": "{{status}}. מטרה: {{text}}" + }, "newSession": { "connectGithubDescription": "חבר את חשבון GitHub דרך הדפדפן, ואז חזור לכאן כדי לבחור מאגר.", "openGithub": "פתיחת GitHub", diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index 15f6c7fb88..514998203f 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -1550,6 +1550,22 @@ "reconnecting": "फिर से कनेक्ट हो रहा है…", "retryConnection": "फिर से कनेक्ट करें" }, + "goal": { + "title": "लक्ष्य", + "statusActive": "सक्रिय लक्ष्य", + "statusPaused": "रुका हुआ लक्ष्य", + "statusComplete": "लक्ष्य पूर्ण", + "statusBlocked": "लक्ष्य अवरुद्ध", + "edit": "लक्ष्य संपादित करें", + "remove": "लक्ष्य हटाएँ", + "pause": "लक्ष्य रोकें", + "resume": "लक्ष्य फिर से शुरू करें", + "editPlaceholder": "लक्ष्य का वर्णन करें", + "removeConfirmTitle": "लक्ष्य हटाएँ?", + "removeConfirmMessage": "CLI इस सत्र का लक्ष्य साफ़ कर देता है।", + "updateFailed": "लक्ष्य अपडेट नहीं किया जा सका।", + "sectionAccessibility": "{{status}}। लक्ष्य: {{text}}" + }, "remoteSession": { "failedToCreate": "रिमोट सत्र नहीं बन सका।", "failedToExit": "सत्र से बाहर नहीं निकल सके।", diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 54ba7639eb..ee52bb3b83 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -2012,6 +2012,22 @@ "connectionLost": "Veza izgubljena", "retryConnection": "Pokušaj se ponovno povezati" }, + "goal": { + "title": "Cilj", + "statusActive": "Aktivan cilj", + "statusPaused": "Pauziran cilj", + "statusComplete": "Cilj dovršen", + "statusBlocked": "Cilj blokiran", + "edit": "Uredi cilj", + "remove": "Ukloni cilj", + "pause": "Pauziraj cilj", + "resume": "Nastavi cilj", + "editPlaceholder": "Opiši cilj", + "removeConfirmTitle": "Ukloniti cilj?", + "removeConfirmMessage": "CLI briše cilj ove sesije.", + "updateFailed": "Cilj nije bilo moguće ažurirati.", + "sectionAccessibility": "{{status}}. Cilj: {{text}}" + }, "newSession": { "connectGithubDescription": "Poveži GitHub u pregledniku, a zatim se vrati ovdje da odabereš repozitorij.", "openGithub": "Otvori GitHub", diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index 1e2a972def..31f804604b 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -1996,6 +1996,22 @@ "connectionLost": "Koneksyon an koupe", "retryConnection": "Rekonekte" }, + "goal": { + "title": "Objektif", + "statusActive": "Objektif aktif", + "statusPaused": "Objektif an pòz", + "statusComplete": "Objektif fini", + "statusBlocked": "Objektif bloke", + "edit": "Modifye objektif la", + "remove": "Retire objektif la", + "pause": "Mete objektif la an pòz", + "resume": "Rekòmanse objektif la", + "editPlaceholder": "Dekri objektif la", + "removeConfirmTitle": "Retire objektif la?", + "removeConfirmMessage": "CLI a efase objektif sesyon sa a.", + "updateFailed": "Pa t kapab mete objektif la ajou.", + "sectionAccessibility": "{{status}}. Objektif: {{text}}" + }, "newSession": { "connectGithubDescription": "Konekte GitHub nan navigatè ou a, epi tounen isit la pou chwazi yon depo.", "openGithub": "Louvri GitHub", diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index f615ee98a0..8a84f3faa5 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -1996,6 +1996,22 @@ "connectionLost": "Megszakadt a kapcsolat", "retryConnection": "Csatlakozás újrapróbálása" }, + "goal": { + "title": "Cél", + "statusActive": "Aktív cél", + "statusPaused": "Szüneteltetett cél", + "statusComplete": "Cél teljesítve", + "statusBlocked": "Cél blokkolva", + "edit": "Cél szerkesztése", + "remove": "Cél eltávolítása", + "pause": "Cél szüneteltetése", + "resume": "Cél folytatása", + "editPlaceholder": "Írja le a célt", + "removeConfirmTitle": "Eltávolítja a célt?", + "removeConfirmMessage": "A CLI törli a munkamenet célját.", + "updateFailed": "A célt nem sikerült frissíteni.", + "sectionAccessibility": "{{status}}. Cél: {{text}}" + }, "newSession": { "connectGithubDescription": "Kapcsold össze a GitHub-fiókodat a böngészőben, majd térj vissza ide, és válassz egy tárolót.", "openGithub": "GitHub megnyitása", diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index e2c15cf473..560f549ef1 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -1996,6 +1996,22 @@ "connectionLost": "Կապը կորել է", "retryConnection": "Կրկին փորձել միանալ" }, + "goal": { + "title": "Նպատակ", + "statusActive": "Ակտիվ նպատակ", + "statusPaused": "Դադարեցված նպատակ", + "statusComplete": "Նպատակն ավարտված է", + "statusBlocked": "Նպատակն արգելափակված է", + "edit": "Խմբագրել նպատակը", + "remove": "Հեռացնել նպատակը", + "pause": "Դադարեցնել նպատակը", + "resume": "Շարունակել նպատակը", + "editPlaceholder": "Նկարագրեք նպատակը", + "removeConfirmTitle": "Հեռացնե՞լ նպատակը", + "removeConfirmMessage": "CLI-ն մաքրում է այս նստաշրջանի նպատակը։", + "updateFailed": "Չհաջողվեց թարմացնել նպատակը։", + "sectionAccessibility": "{{status}}։ Նպատակ՝ {{text}}" + }, "newSession": { "connectGithubDescription": "Միացրեք GitHub-ը ձեր զննարկչում, ապա վերադարձեք այստեղ՝ պահոց ընտրելու համար։", "openGithub": "Բացել GitHub-ը", diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index 27b9d94345..a83d18ec29 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -1562,6 +1562,22 @@ "connectionLost": "Koneksi terputus", "retryConnection": "Coba sambungkan kembali" }, + "goal": { + "title": "Tujuan", + "statusActive": "Tujuan aktif", + "statusPaused": "Tujuan dijeda", + "statusComplete": "Tujuan selesai", + "statusBlocked": "Tujuan diblokir", + "edit": "Edit tujuan", + "remove": "Hapus tujuan", + "pause": "Jeda tujuan", + "resume": "Lanjutkan tujuan", + "editPlaceholder": "Jelaskan tujuan", + "removeConfirmTitle": "Hapus tujuan?", + "removeConfirmMessage": "CLI menghapus tujuan sesi ini.", + "updateFailed": "Tujuan tidak dapat diperbarui.", + "sectionAccessibility": "{{status}}. Tujuan: {{text}}" + }, "newSession": { "connectGithubDescription": "Hubungkan GitHub melalui peramban, lalu kembali ke sini untuk memilih repositori.", "openGithub": "Buka GitHub", diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index 6057842a14..058f150eec 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -1996,6 +1996,22 @@ "connectionLost": "Njikọ akwụsịla", "retryConnection": "Nwaa njikọ ọzọ" }, + "goal": { + "title": "Ebumnuche", + "statusActive": "Ebumnuche na-arụ ọrụ", + "statusPaused": "Ebumnuche a kwụsịrị", + "statusComplete": "Ebumnuche zuru ezu", + "statusBlocked": "Ebumnuche egbochiri", + "edit": "Dezie ebumnuche", + "remove": "Wepu ebumnuche", + "pause": "Kwụsị ebumnuche", + "resume": "Maliteghachi ebumnuche", + "editPlaceholder": "Kọwaa ebumnuche", + "removeConfirmTitle": "Wepu ebumnuche?", + "removeConfirmMessage": "CLI na-ehichapụ ebumnuche nke nnọkọ a.", + "updateFailed": "Enweghị ike imelite ebumnuche.", + "sectionAccessibility": "{{status}}. Ebumnuche: {{text}}" + }, "newSession": { "connectGithubDescription": "Jikọọ GitHub n'ihe nchọgharị gị, wee laghachi ebe a ịhọrọ ebe nchekwa koodu.", "openGithub": "Mepee GitHub", diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index f314dc8482..58c0260211 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -1996,6 +1996,22 @@ "connectionLost": "Tenging rofnaði", "retryConnection": "Reyna að tengjast aftur" }, + "goal": { + "title": "Markmið", + "statusActive": "Virkt markmið", + "statusPaused": "Markmið í bið", + "statusComplete": "Markmiði lokið", + "statusBlocked": "Markmið læst", + "edit": "Breyta markmiði", + "remove": "Fjarlægja markmið", + "pause": "Setja markmið í bið", + "resume": "Halda markmiði áfram", + "editPlaceholder": "Lýstu markmiðinu", + "removeConfirmTitle": "Fjarlægja markmið?", + "removeConfirmMessage": "CLI hreinsar markmið þessarar lotu.", + "updateFailed": "Ekki tókst að uppfæra markmiðið.", + "sectionAccessibility": "{{status}}. Markmið: {{text}}" + }, "newSession": { "connectGithubDescription": "Tengdu GitHub í vafranum þínum og komdu svo aftur hingað til að velja kóðasafn.", "openGithub": "Opna GitHub", diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index a1c573738b..46db7de76f 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -1542,6 +1542,22 @@ "connectionLost": "Connessione persa", "retryConnection": "Riprova a connetterti" }, + "goal": { + "title": "Obiettivo", + "statusActive": "Obiettivo attivo", + "statusPaused": "Obiettivo in pausa", + "statusComplete": "Obiettivo completato", + "statusBlocked": "Obiettivo bloccato", + "edit": "Modifica obiettivo", + "remove": "Rimuovi obiettivo", + "pause": "Metti in pausa l'obiettivo", + "resume": "Riprendi l'obiettivo", + "editPlaceholder": "Descrivi l'obiettivo", + "removeConfirmTitle": "Rimuovere l'obiettivo?", + "removeConfirmMessage": "Il CLI cancella l'obiettivo di questa sessione.", + "updateFailed": "Impossibile aggiornare l'obiettivo.", + "sectionAccessibility": "{{status}}. Obiettivo: {{text}}" + }, "newSession": { "connectGithubDescription": "Collega GitHub nel browser, poi torna qui per scegliere un repository.", "openGithub": "Apri GitHub", diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index de708b2f9e..f947916e5d 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -1562,6 +1562,22 @@ "connectionLost": "接続切断", "retryConnection": "接続を再試行" }, + "goal": { + "title": "ゴール", + "statusActive": "アクティブなゴール", + "statusPaused": "一時停止中のゴール", + "statusComplete": "ゴール完了", + "statusBlocked": "ゴールがブロックされました", + "edit": "ゴールを編集", + "remove": "ゴールを削除", + "pause": "ゴールを一時停止", + "resume": "ゴールを再開", + "editPlaceholder": "ゴールを説明", + "removeConfirmTitle": "ゴールを削除しますか?", + "removeConfirmMessage": "CLI がこのセッションのゴールを消去します。", + "updateFailed": "ゴールを更新できませんでした。", + "sectionAccessibility": "{{status}}。ゴール: {{text}}" + }, "newSession": { "connectGithubDescription": "ブラウザーでGitHubと連携し、この画面に戻ってリポジトリを選択してください。", "openGithub": "GitHubを開く", diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index 1288699e0b..756e467420 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -1996,6 +1996,22 @@ "connectionLost": "კავშირი გაწყდა", "retryConnection": "ხელახლა დაკავშირება" }, + "goal": { + "title": "მიზანი", + "statusActive": "აქტიური მიზანი", + "statusPaused": "შეჩერებული მიზანი", + "statusComplete": "მიზანი დასრულებულია", + "statusBlocked": "მიზანი დაბლოკილია", + "edit": "მიზნის რედაქტირება", + "remove": "მიზნის წაშლა", + "pause": "მიზნის შეჩერება", + "resume": "მიზნის გაგრძელება", + "editPlaceholder": "აღწერეთ მიზანი", + "removeConfirmTitle": "წავშალოთ მიზანი?", + "removeConfirmMessage": "CLI ასუფთავებს ამ სესიის მიზანს.", + "updateFailed": "მიზნის განახლება ვერ მოხერხდა.", + "sectionAccessibility": "{{status}}. მიზანი: {{text}}" + }, "newSession": { "connectGithubDescription": "ბრაუზერში დააკავშირეთ GitHub-ის ანგარიში, შემდეგ აქ დაბრუნდით რეპოზიტორიის ასარჩევად.", "openGithub": "GitHub-ის გახსნა", diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index d7808a940f..81af97ef51 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -1996,6 +1996,22 @@ "connectionLost": "Қосылым үзілді", "retryConnection": "Қайта қосылу" }, + "goal": { + "title": "Мақсат", + "statusActive": "Белсенді мақсат", + "statusPaused": "Тоқтатылған мақсат", + "statusComplete": "Мақсат орындалды", + "statusBlocked": "Мақсат бұғатталды", + "edit": "Мақсатты өңдеу", + "remove": "Мақсатты жою", + "pause": "Мақсатты тоқтату", + "resume": "Мақсатты жалғастыру", + "editPlaceholder": "Мақсатты сипаттаңыз", + "removeConfirmTitle": "Мақсат жойылсын ба?", + "removeConfirmMessage": "CLI осы сеанстың мақсатын тазалайды.", + "updateFailed": "Мақсатты жаңарту мүмкін болмады.", + "sectionAccessibility": "{{status}}. Мақсат: {{text}}" + }, "newSession": { "connectGithubDescription": "Браузерде GitHub тіркелгісін байланыстырып, репозиторий таңдау үшін осында оралыңыз.", "openGithub": "GitHub-ты ашу", diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index f4430411d0..f1a136714a 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -1996,6 +1996,22 @@ "connectionLost": "ដាច់ការតភ្ជាប់", "retryConnection": "ព្យាយាមភ្ជាប់ម្តងទៀត" }, + "goal": { + "title": "គោលដៅ", + "statusActive": "គោលដៅសកម្ម", + "statusPaused": "គោលដៅផ្អាក", + "statusComplete": "គោលដៅបានបញ្ចប់", + "statusBlocked": "គោលដៅត្រូវបានទប់ស្កាត់", + "edit": "កែសម្រួលគោលដៅ", + "remove": "ដកគោលដៅចេញ", + "pause": "ផ្អាកគោលដៅ", + "resume": "បន្តគោលដៅ", + "editPlaceholder": "ពិពណ៌នាគោលដៅ", + "removeConfirmTitle": "ដកគោលដៅចេញ?", + "removeConfirmMessage": "CLI សម្អាតគោលដៅរបស់វគ្គនេះ។", + "updateFailed": "មិនអាចធ្វើបច្ចុប្បន្នភាពគោលដៅបានទេ។", + "sectionAccessibility": "{{status}}។ គោលដៅ៖ {{text}}" + }, "newSession": { "connectGithubDescription": "ភ្ជាប់ GitHub ក្នុងកម្មវិធីរុករករបស់អ្នក បន្ទាប់មកត្រឡប់មកទីនេះ ដើម្បីជ្រើសរើសឃ្លាំងកូដ។", "openGithub": "បើក GitHub", diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index 26cd19f7fa..1b54bd156f 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -1996,6 +1996,22 @@ "connectionLost": "ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ", "retryConnection": "ಮತ್ತೆ ಸಂಪರ್ಕಿಸಲು ಪ್ರಯತ್ನಿಸಿ" }, + "goal": { + "title": "ಗುರಿ", + "statusActive": "ಸಕ್ರಿಯ ಗುರಿ", + "statusPaused": "ವಿರಾಮಗೊಳಿಸಿದ ಗುರಿ", + "statusComplete": "ಗುರಿ ಪೂರ್ಣಗೊಂಡಿದೆ", + "statusBlocked": "ಗುರಿ ನಿರ್ಬಂಧಿಸಲಾಗಿದೆ", + "edit": "ಗುರಿ ಸಂಪಾದಿಸಿ", + "remove": "ಗುರಿ ತೆಗೆದುಹಾಕಿ", + "pause": "ಗುರಿ ವಿರಾಮಗೊಳಿಸಿ", + "resume": "ಗುರಿ ಪುನರಾರಂಭಿಸಿ", + "editPlaceholder": "ಗುರಿಯನ್ನು ವಿವರಿಸಿ", + "removeConfirmTitle": "ಗುರಿ ತೆಗೆದುಹಾಕಬೇಕೆ?", + "removeConfirmMessage": "CLI ಈ ಅಧಿವೇಶನದ ಗುರಿಯನ್ನು ಅಳಿಸುತ್ತದೆ.", + "updateFailed": "ಗುರಿಯನ್ನು ನವೀಕರಿಸಲಾಗಲಿಲ್ಲ.", + "sectionAccessibility": "{{status}}. ಗುರಿ: {{text}}" + }, "newSession": { "connectGithubDescription": "ನಿಮ್ಮ ಬ್ರೌಸರ್‌ನಲ್ಲಿ GitHub ಸಂಪರ್ಕಿಸಿ. ನಂತರ ರೆಪೊಸಿಟರಿಯನ್ನು ಆಯ್ಕೆಮಾಡಲು ಇಲ್ಲಿಗೆ ಹಿಂತಿರುಗಿ.", "openGithub": "GitHub ತೆರೆಯಿರಿ", diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 4ec675056d..7881cb2a01 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -1562,6 +1562,22 @@ "connectionLost": "연결 끊김", "retryConnection": "연결 다시 시도" }, + "goal": { + "title": "목표", + "statusActive": "활성 목표", + "statusPaused": "일시 중지된 목표", + "statusComplete": "목표 완료", + "statusBlocked": "목표 차단됨", + "edit": "목표 편집", + "remove": "목표 제거", + "pause": "목표 일시 중지", + "resume": "목표 재개", + "editPlaceholder": "목표 설명", + "removeConfirmTitle": "목표를 제거할까요?", + "removeConfirmMessage": "CLI가 이 세션의 목표를 지웁니다.", + "updateFailed": "목표를 업데이트할 수 없습니다.", + "sectionAccessibility": "{{status}}. 목표: {{text}}" + }, "newSession": { "connectGithubDescription": "브라우저에서 GitHub를 연결한 후 여기로 돌아와 저장소를 선택하세요.", "openGithub": "GitHub 열기", diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index d353393a05..87d0df93b8 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -1996,6 +1996,22 @@ "connectionLost": "ການເຊື່ອມຕໍ່ຂາດ", "retryConnection": "ລອງເຊື່ອມຕໍ່ໃໝ່" }, + "goal": { + "title": "ເປົ້າໝາຍ", + "statusActive": "ເປົ້າໝາຍທີ່ເຄື່ອນໄຫວ", + "statusPaused": "ເປົ້າໝາຍທີ່ຢຸດຊົ່ວຄາວ", + "statusComplete": "ເປົ້າໝາຍສຳເລັດ", + "statusBlocked": "ເປົ້າໝາຍຖືກສະກັດ", + "edit": "ແກ້ໄຂເປົ້າໝາຍ", + "remove": "ລຶບເປົ້າໝາຍ", + "pause": "ຢຸດເປົ້າໝາຍຊົ່ວຄາວ", + "resume": "ສືບຕໍ່ເປົ້າໝາຍ", + "editPlaceholder": "ອະທິບາຍເປົ້າໝາຍ", + "removeConfirmTitle": "ລຶບເປົ້າໝາຍບໍ?", + "removeConfirmMessage": "CLI ລຶບເປົ້າໝາຍຂອງເຊດຊັນນີ້.", + "updateFailed": "ບໍ່ສາມາດອັບເດດເປົ້າໝາຍໄດ້.", + "sectionAccessibility": "{{status}}. ເປົ້າໝາຍ: {{text}}" + }, "newSession": { "connectGithubDescription": "ເຊື່ອມຕໍ່ GitHub ໃນບຣາວເຊີ ແລ້ວກັບມາທີ່ນີ້ເພື່ອເລືອກຄັງໂຄດ.", "openGithub": "ເປີດ GitHub", diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index 011089132c..75bf6f2907 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -2028,6 +2028,22 @@ "connectionLost": "Ryšys nutrūko", "retryConnection": "Bandyti prisijungti iš naujo" }, + "goal": { + "title": "Tikslas", + "statusActive": "Aktyvus tikslas", + "statusPaused": "Pristabdytas tikslas", + "statusComplete": "Tikslas įvykdytas", + "statusBlocked": "Tikslas užblokuotas", + "edit": "Redaguoti tikslą", + "remove": "Pašalinti tikslą", + "pause": "Pristabdyti tikslą", + "resume": "Tęsti tikslą", + "editPlaceholder": "Aprašykite tikslą", + "removeConfirmTitle": "Pašalinti tikslą?", + "removeConfirmMessage": "CLI išvalo šios sesijos tikslą.", + "updateFailed": "Nepavyko atnaujinti tikslo.", + "sectionAccessibility": "{{status}}. Tikslas: {{text}}" + }, "newSession": { "connectGithubDescription": "Naršyklėje susiekite GitHub paskyrą, tada grįžkite čia ir pasirinkite saugyklą.", "openGithub": "Atidaryti GitHub", diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index f3232d2b88..adea148ac7 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -2012,6 +2012,22 @@ "connectionLost": "Savienojums pārtrūcis", "retryConnection": "Izveidot savienojumu vēlreiz" }, + "goal": { + "title": "Mērķis", + "statusActive": "Aktīvs mērķis", + "statusPaused": "Apturēts mērķis", + "statusComplete": "Mērķis izpildīts", + "statusBlocked": "Mērķis bloķēts", + "edit": "Rediģēt mērķi", + "remove": "Noņemt mērķi", + "pause": "Apturēt mērķi", + "resume": "Turpināt mērķi", + "editPlaceholder": "Aprakstiet mērķi", + "removeConfirmTitle": "Noņemt mērķi?", + "removeConfirmMessage": "CLI notīra šīs sesijas mērķi.", + "updateFailed": "Mērķi neizdevās atjaunināt.", + "sectionAccessibility": "{{status}}. Mērķis: {{text}}" + }, "newSession": { "connectGithubDescription": "Pievieno GitHub kontu pārlūkā, pēc tam atgriezies šeit, lai izvēlētos repozitoriju.", "openGithub": "Atvērt GitHub", diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index bb72796ab5..ed610fc977 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -1996,6 +1996,22 @@ "connectionLost": "Tapaka ny fifandraisana", "retryConnection": "Andramo mifandray indray" }, + "goal": { + "title": "Tanjona", + "statusActive": "Tanjona mavitrika", + "statusPaused": "Tanjona mihajanona", + "statusComplete": "Tanjona vita", + "statusBlocked": "Tanjona voasakana", + "edit": "Ovay ny tanjona", + "remove": "Esory ny tanjona", + "pause": "Ajanony ny tanjona", + "resume": "Tohizo ny tanjona", + "editPlaceholder": "Farano ny tanjona", + "removeConfirmTitle": "Esorina ny tanjona?", + "removeConfirmMessage": "Mamafa ny tanjon'ity session ity ny CLI.", + "updateFailed": "Tsy afaka nanavao ny tanjona.", + "sectionAccessibility": "{{status}}. Tanjona: {{text}}" + }, "newSession": { "connectGithubDescription": "Ampifandraiso ny GitHub ao amin'ny mpitety tranonkalanao, dia miverena eto hisafidy tahirin-kaody.", "openGithub": "Sokafy ny GitHub", diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index 2857f4299d..bd57fc36cf 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -1996,6 +1996,22 @@ "connectionLost": "Kua motu te hononga", "retryConnection": "Whakamātau ki te hono anō" }, + "goal": { + "title": "Whāinga", + "statusActive": "Whāinga hohe", + "statusPaused": "Whāinga kua whakatārewa", + "statusComplete": "Kua oti te whāinga", + "statusBlocked": "Kua āraia te whāinga", + "edit": "Whakatika whāinga", + "remove": "Tango whāinga", + "pause": "Whakatārewa whāinga", + "resume": "Haere tonu te whāinga", + "editPlaceholder": "Whakamārama te whāinga", + "removeConfirmTitle": "Tango whāinga?", + "removeConfirmMessage": "Ka muru te CLI i te whāinga o tēnei wātū.", + "updateFailed": "Kāore i taea te whakahōu te whāinga.", + "sectionAccessibility": "{{status}}. Whāinga: {{text}}" + }, "newSession": { "connectGithubDescription": "Honohia a GitHub mā tō pūtirotiro, kātahi ka hoki mai ki konei ki te kōwhiri i tētahi putunga waehere.", "openGithub": "Whakatuwheratia a GitHub", diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index ec86b95f49..90a7f2f875 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -1996,6 +1996,22 @@ "connectionLost": "Прекината врска", "retryConnection": "Поврзи се повторно" }, + "goal": { + "title": "Цел", + "statusActive": "Активна цел", + "statusPaused": "Паузирана цел", + "statusComplete": "Целта е завршена", + "statusBlocked": "Целта е блокирана", + "edit": "Уреди цел", + "remove": "Отстрани цел", + "pause": "Паузирај цел", + "resume": "Продолжи цел", + "editPlaceholder": "Опишете ја целта", + "removeConfirmTitle": "Да се отстрани целта?", + "removeConfirmMessage": "CLI ја брише целта на оваа сесија.", + "updateFailed": "Целта не можеше да се ажурира.", + "sectionAccessibility": "{{status}}. Цел: {{text}}" + }, "newSession": { "connectGithubDescription": "Поврзи го GitHub преку прелистувачот, па врати се тука за да избереш репозиториум.", "openGithub": "Отвори GitHub", diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 0fb6444f75..74ce3ef394 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -1996,6 +1996,22 @@ "connectionLost": "കണക്ഷൻ നഷ്ടപ്പെട്ടു", "retryConnection": "വീണ്ടും കണക്റ്റ് ചെയ്യാൻ ശ്രമിക്കുക" }, + "goal": { + "title": "ലക്ഷ്യം", + "statusActive": "സജീവ ലക്ഷ്യം", + "statusPaused": "താൽക്കാലികമായി നിർത്തിയ ലക്ഷ്യം", + "statusComplete": "ലക്ഷ്യം പൂർത്തിയായി", + "statusBlocked": "ലക്ഷ്യം തടഞ്ഞു", + "edit": "ലക്ഷ്യം എഡിറ്റ് ചെയ്യുക", + "remove": "ലക്ഷ്യം നീക്കുക", + "pause": "ലക്ഷ്യം താൽക്കാലികമായി നിർത്തുക", + "resume": "ലക്ഷ്യം തുടരുക", + "editPlaceholder": "ലക്ഷ്യം വിവരിക്കുക", + "removeConfirmTitle": "ലക്ഷ്യം നീക്കണോ?", + "removeConfirmMessage": "ഈ സെഷന്റെ ലക്ഷ്യം CLI മായ്ക്കുന്നു.", + "updateFailed": "ലക്ഷ്യം അപ്ഡേറ്റ് ചെയ്യാനായില്ല.", + "sectionAccessibility": "{{status}}. ലക്ഷ്യം: {{text}}" + }, "newSession": { "connectGithubDescription": "ബ്രൗസറിൽ GitHub കണക്റ്റ് ചെയ്യുക. തുടർന്ന് ഒരു റിപ്പോസിറ്ററി തിരഞ്ഞെടുക്കാൻ ഇവിടെ മടങ്ങിയെത്തുക.", "openGithub": "GitHub തുറക്കുക", diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index eba6e9d3e4..11c36cbb98 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -1996,6 +1996,22 @@ "connectionLost": "Холболт тасарсан", "retryConnection": "Дахин холбогдох" }, + "goal": { + "title": "Зорилго", + "statusActive": "Идэвхтэй зорилго", + "statusPaused": "Түр зогссон зорилго", + "statusComplete": "Зорилго биелэв", + "statusBlocked": "Зорилго хаагдсан", + "edit": "Зорилгыг засах", + "remove": "Зорилгыг устгах", + "pause": "Зорилгыг түр зогсоох", + "resume": "Зорилгыг үргэлжлүүлэх", + "editPlaceholder": "Зорилгыг тайлбарлана уу", + "removeConfirmTitle": "Зорилгыг устгах уу?", + "removeConfirmMessage": "CLI энэ сессийн зорилгыг арилгана.", + "updateFailed": "Зорилгыг шинэчлэх боломжгүй.", + "sectionAccessibility": "{{status}}. Зорилго: {{text}}" + }, "newSession": { "connectGithubDescription": "Хөтөч дээрээ GitHub-ыг холбоод, энд буцаж ирж кодын сангаа сонгоно уу.", "openGithub": "GitHub-ыг нээх", diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index 7a299dba55..ecb78a5f00 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -1996,6 +1996,22 @@ "connectionLost": "कनेक्शन तुटले", "retryConnection": "पुन्हा कनेक्ट करण्याचा प्रयत्न करा" }, + "goal": { + "title": "ध्येय", + "statusActive": "सक्रिय ध्येय", + "statusPaused": "थांबवलेले ध्येय", + "statusComplete": "ध्येय पूर्ण", + "statusBlocked": "ध्येय अवरोधित", + "edit": "ध्येय संपादित करा", + "remove": "ध्येय काढा", + "pause": "ध्येय थांबवा", + "resume": "ध्येय पुन्हा सुरू करा", + "editPlaceholder": "ध्येयाचे वर्णन करा", + "removeConfirmTitle": "ध्येय काढायचे?", + "removeConfirmMessage": "CLI या सत्राचे ध्येय साफ करते.", + "updateFailed": "ध्येय अपडेट करता आले नाही.", + "sectionAccessibility": "{{status}}. ध्येय: {{text}}" + }, "newSession": { "connectGithubDescription": "तुमच्या ब्राउझरमध्ये GitHub कनेक्ट करा, नंतर रिपॉझिटरी निवडण्यासाठी येथे परत या.", "openGithub": "GitHub उघडा", diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index aa9ce01a63..28bc96c58a 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -1996,6 +1996,22 @@ "connectionLost": "Sambungan terputus", "retryConnection": "Cuba sambung semula" }, + "goal": { + "title": "Matlamat", + "statusActive": "Matlamat aktif", + "statusPaused": "Matlamat dijeda", + "statusComplete": "Matlamat selesai", + "statusBlocked": "Matlamat disekat", + "edit": "Edit matlamat", + "remove": "Buang matlamat", + "pause": "Jeda matlamat", + "resume": "Sambung matlamat", + "editPlaceholder": "Terangkan matlamat", + "removeConfirmTitle": "Buang matlamat?", + "removeConfirmMessage": "CLI menghapuskan matlamat sesi ini.", + "updateFailed": "Matlamat tidak dapat dikemas kini.", + "sectionAccessibility": "{{status}}. Matlamat: {{text}}" + }, "newSession": { "connectGithubDescription": "Sambungkan GitHub dalam pelayar anda, kemudian kembali ke sini untuk memilih repositori.", "openGithub": "Buka GitHub", diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index ae735592d2..06590b4c9f 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -2044,6 +2044,22 @@ "connectionLost": "Il-konnessjoni ntilfet", "retryConnection": "Erġa' pprova taqbad" }, + "goal": { + "title": "Għan", + "statusActive": "Għan attiv", + "statusPaused": "Għan imwaqqaf", + "statusComplete": "Għan komplut", + "statusBlocked": "Għan imblukkat", + "edit": "Editja l-għan", + "remove": "Neħħi l-għan", + "pause": "Waqqaf l-għan", + "resume": "Ibda l-għan mill-ġdid", + "editPlaceholder": "Iddeskrivi l-għan", + "removeConfirmTitle": "Neħħi l-għan?", + "removeConfirmMessage": "Il-CLI iħassar l-għan ta' din is-sessjoni.", + "updateFailed": "Ma setax jiġi aġġornat l-għan.", + "sectionAccessibility": "{{status}}. Għan: {{text}}" + }, "newSession": { "connectGithubDescription": "Qabbad GitHub fil-brawżer tiegħek, imbagħad erġa' lura hawn biex tagħżel repożitorju.", "openGithub": "Iftaħ GitHub", diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index 685869ec66..6973776470 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -1996,6 +1996,22 @@ "connectionLost": "ချိတ်ဆက်မှု ပျက်သွားသည်", "retryConnection": "ထပ်ချိတ်ဆက်ကြည့်ပါ" }, + "goal": { + "title": "ပန်းတိုင်", + "statusActive": "အသုံးပြုနေသော ပန်းတိုင်", + "statusPaused": "ရပ်ဆိုင်းထားသော ပန်းတိုင်", + "statusComplete": "ပန်းတိုင် ပြီးမြောက်ပါပြီ", + "statusBlocked": "ပန်းတိုင် ပိတ်ဆို့ထားသည်", + "edit": "ပန်းတိုင် တည်းဖြတ်ရန်", + "remove": "ပန်းတိုင် ဖယ်ရှားရန်", + "pause": "ပန်းတိုင် ရပ်ဆိုင်းရန်", + "resume": "ပန်းတိုင် ပြန်လည်စတင်ရန်", + "editPlaceholder": "ပန်းတိုင်ကို ဖော်ပြပါ", + "removeConfirmTitle": "ပန်းတိုင် ဖယ်ရှားမလား?", + "removeConfirmMessage": "CLI သည် ဤစက်ရှင်၏ ပန်းတိုင်ကို ရှင်းလင်းသည်။", + "updateFailed": "ပန်းတိုင်ကို အပ်ဒိတ်လုပ်နိုင်ခြင်း မရှိပါ။", + "sectionAccessibility": "{{status}}။ ပန်းတိုင်: {{text}}" + }, "newSession": { "connectGithubDescription": "ဘရောက်ဆာတွင် GitHub ကို ချိတ်ဆက်ပါ။ ထို့နောက် ကုဒ်သိုလှောင်ခန်းတစ်ခု ရွေးရန် ဤနေရာသို့ ပြန်လာပါ။", "openGithub": "GitHub ဖွင့်ပါ", diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index 81d59fb3b7..77b0610aa6 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -1996,6 +1996,22 @@ "connectionLost": "Tilkoblingen er brutt", "retryConnection": "Prøv å koble til på nytt" }, + "goal": { + "title": "Mål", + "statusActive": "Aktivt mål", + "statusPaused": "Pauset mål", + "statusComplete": "Mål fullført", + "statusBlocked": "Mål blokkert", + "edit": "Rediger mål", + "remove": "Fjern mål", + "pause": "Sett målet på pause", + "resume": "Gjenoppta mål", + "editPlaceholder": "Beskriv målet", + "removeConfirmTitle": "Fjern mål?", + "removeConfirmMessage": "CLI sletter målet for denne økten.", + "updateFailed": "Målet kunne ikke oppdateres.", + "sectionAccessibility": "{{status}}. Mål: {{text}}" + }, "newSession": { "connectGithubDescription": "Koble til GitHub i nettleseren. Gå deretter tilbake hit for å velge et kodelager.", "openGithub": "Åpne GitHub", diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index a1f3d06381..4af29d7c0e 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -1996,6 +1996,22 @@ "connectionLost": "जडान टुट्यो", "retryConnection": "जडान गर्न पुनः प्रयास गर्नुहोस्" }, + "goal": { + "title": "लक्ष्य", + "statusActive": "सक्रिय लक्ष्य", + "statusPaused": "रोकिएको लक्ष्य", + "statusComplete": "लक्ष्य पूरा भयो", + "statusBlocked": "लक्ष्य अवरुद्ध", + "edit": "लक्ष्य सम्पादन गर्नुहोस्", + "remove": "लक्ष्य हटाउनुहोस्", + "pause": "लक्ष्य रोक्नुहोस्", + "resume": "लक्ष्य पुनः सुरु गर्नुहोस्", + "editPlaceholder": "लक्ष्य वर्णन गर्नुहोस्", + "removeConfirmTitle": "लक्ष्य हटाउने?", + "removeConfirmMessage": "CLI ले यो सत्रको लक्ष्य हटाउँछ।", + "updateFailed": "लक्ष्य अद्यावधिक गर्न सकिएन।", + "sectionAccessibility": "{{status}}। लक्ष्य: {{text}}" + }, "newSession": { "connectGithubDescription": "आफ्नो ब्राउजरमा GitHub जडान गर्नुहोस्। त्यसपछि रिपोजिटरी छान्न यहाँ फर्कनुहोस्।", "openGithub": "GitHub खोल्नुहोस्", diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index d4a7ab6043..9fda590f62 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -1562,6 +1562,22 @@ "connectionLost": "Verbinding verbroken", "retryConnection": "Opnieuw verbinden" }, + "goal": { + "title": "Doel", + "statusActive": "Actief doel", + "statusPaused": "Gepauzeerd doel", + "statusComplete": "Doel voltooid", + "statusBlocked": "Doel geblokkeerd", + "edit": "Doel bewerken", + "remove": "Doel verwijderen", + "pause": "Doel pauzeren", + "resume": "Doel hervatten", + "editPlaceholder": "Beschrijf het doel", + "removeConfirmTitle": "Doel verwijderen?", + "removeConfirmMessage": "De CLI wist het doel van deze sessie.", + "updateFailed": "Het doel kon niet worden bijgewerkt.", + "sectionAccessibility": "{{status}}. Doel: {{text}}" + }, "newSession": { "connectGithubDescription": "Koppel GitHub in je browser en kom hier terug om een repository te kiezen.", "openGithub": "GitHub openen", diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index ca03613fdc..b4365e17b1 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -1996,6 +1996,22 @@ "connectionLost": "Walqabannaan citeera", "retryConnection": "Deebi'ii walqabsiisuuf yaali" }, + "goal": { + "title": "Kaayyoo", + "statusActive": "Kaayyoo socho'aa", + "statusPaused": "Kaayyoo dhaabbate", + "statusComplete": "Kaayyoon xumurame", + "statusBlocked": "Kaayyoon cufame", + "edit": "Kaayyoo gulaali", + "remove": "Kaayyoo haqi", + "pause": "Kaayyoo dhaabi", + "resume": "Kaayyoo itti fufi", + "editPlaceholder": "Kaayyoo ibsi", + "removeConfirmTitle": "Kaayyoo haquu?", + "removeConfirmMessage": "CLI kaayyoo seesshinii kanaa haqa.", + "updateFailed": "Kaayyoo haaromsuun hin danda'amne.", + "sectionAccessibility": "{{status}}. Kaayyoo: {{text}}" + }, "newSession": { "connectGithubDescription": "GitHub biraawzarii kee keessatti walqabsiisi, sana booda kuusaa koodii filachuuf asitti deebi'i.", "openGithub": "GitHub bani", diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index 5f634108bd..6aab5d42bd 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -1996,6 +1996,22 @@ "connectionLost": "ସଂଯୋଗ ବିଚ୍ଛିନ୍ନ", "retryConnection": "ପୁଣି ସଂଯୋଗ କରନ୍ତୁ" }, + "goal": { + "title": "ଲକ୍ଷ୍ୟ", + "statusActive": "ସକ୍ରିୟ ଲକ୍ଷ୍ୟ", + "statusPaused": "ବିରତ ଲକ୍ଷ୍ୟ", + "statusComplete": "ଲକ୍ଷ୍ୟ ସମ୍ପୂର୍ଣ୍ଣ", + "statusBlocked": "ଲକ୍ଷ୍ୟ ଅବରୋଧିତ", + "edit": "ଲକ୍ଷ୍ୟ ସମ୍ପାଦନ କରନ୍ତୁ", + "remove": "ଲକ୍ଷ୍ୟ ହଟାନ୍ତୁ", + "pause": "ଲକ୍ଷ୍ୟ ବିରତ କରନ୍ତୁ", + "resume": "ଲକ୍ଷ୍ୟ ପୁନଃ ଆରମ୍ଭ କରନ୍ତୁ", + "editPlaceholder": "ଲକ୍ଷ୍ୟ ବର୍ଣ୍ଣନା କରନ୍ତୁ", + "removeConfirmTitle": "ଲକ୍ଷ୍ୟ ହଟାଇବେ?", + "removeConfirmMessage": "CLI ଏହି ସେସନର ଲକ୍ଷ୍ୟ ଲିଭାଇଦିଏ।", + "updateFailed": "ଲକ୍ଷ୍ୟ ଅପଡେଟ୍ କରାଯାଇପାରିଲା ନାହିଁ।", + "sectionAccessibility": "{{status}}। ଲକ୍ଷ୍ୟ: {{text}}" + }, "newSession": { "connectGithubDescription": "ଆପଣଙ୍କ ବ୍ରାଉଜରରେ GitHub ସଂଯୋଗ କରନ୍ତୁ, ତା'ପରେ ଏକ ରିପୋଜିଟୋରୀ ବାଛିବାକୁ ଏଠାକୁ ଫେରନ୍ତୁ।", "openGithub": "GitHub ଖୋଲନ୍ତୁ", diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index c071e8745c..42bad9cadd 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -1996,6 +1996,22 @@ "connectionLost": "ਕਨੈਕਸ਼ਨ ਟੁੱਟ ਗਿਆ", "retryConnection": "ਮੁੜ ਕਨੈਕਟ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ" }, + "goal": { + "title": "ਟੀਚਾ", + "statusActive": "ਸਰਗਰਮ ਟੀਚਾ", + "statusPaused": "ਰੋਕਿਆ ਟੀਚਾ", + "statusComplete": "ਟੀਚਾ ਪੂਰਾ ਹੋਇਆ", + "statusBlocked": "ਟੀਚਾ ਰੋਕਿਆ ਗਿਆ", + "edit": "ਟੀਚਾ ਸੰਪਾਦਿਤ ਕਰੋ", + "remove": "ਟੀਚਾ ਹਟਾਓ", + "pause": "ਟੀਚਾ ਰੋਕੋ", + "resume": "ਟੀਚਾ ਮੁੜ ਸ਼ੁਰੂ ਕਰੋ", + "editPlaceholder": "ਟੀਚੇ ਦਾ ਵਰਣਨ ਕਰੋ", + "removeConfirmTitle": "ਟੀਚਾ ਹਟਾਉਣਾ?", + "removeConfirmMessage": "CLI ਇਸ ਸੈਸ਼ਨ ਦਾ ਟੀਚਾ ਮਿਟਾਉਂਦਾ ਹੈ।", + "updateFailed": "ਟੀਚਾ ਅੱਪਡੇਟ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ।", + "sectionAccessibility": "{{status}}। ਟੀਚਾ: {{text}}" + }, "newSession": { "connectGithubDescription": "ਆਪਣੇ ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ GitHub ਨੂੰ ਕਨੈਕਟ ਕਰੋ, ਫਿਰ ਰਿਪੋਜ਼ਟਰੀ ਚੁਣਨ ਲਈ ਇੱਥੇ ਵਾਪਸ ਆਓ।", "openGithub": "GitHub ਖੋਲ੍ਹੋ", diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index c3ced2bca2..bfb51a0704 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -1580,6 +1580,22 @@ "connectionLost": "Utracono połączenie", "retryConnection": "Połącz ponownie" }, + "goal": { + "title": "Cel", + "statusActive": "Aktywny cel", + "statusPaused": "Wstrzymany cel", + "statusComplete": "Cel ukończony", + "statusBlocked": "Cel zablokowany", + "edit": "Edytuj cel", + "remove": "Usuń cel", + "pause": "Wstrzymaj cel", + "resume": "Wznów cel", + "editPlaceholder": "Opisz cel", + "removeConfirmTitle": "Usunąć cel?", + "removeConfirmMessage": "CLI czyści cel tej sesji.", + "updateFailed": "Nie udało się zaktualizować celu.", + "sectionAccessibility": "{{status}}. Cel: {{text}}" + }, "newSession": { "connectGithubDescription": "Połącz konto GitHub w przeglądarce, a następnie wróć tutaj, aby wybrać repozytorium.", "openGithub": "Otwórz GitHub", diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index cdcaa78a27..78476f5e87 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -1996,6 +1996,22 @@ "connectionLost": "اړیکه پرې شوه", "retryConnection": "بیا د نښلېدو هڅه وکړئ" }, + "goal": { + "title": "هدف", + "statusActive": "فعال هدف", + "statusPaused": "ځنډول شوی هدف", + "statusComplete": "هدف بشپړ شو", + "statusBlocked": "هدف بند شو", + "edit": "هدف سمول", + "remove": "هدف لرې کول", + "pause": "هدف ځنډول", + "resume": "هدف بیا پیلول", + "editPlaceholder": "هدف تشریح کړئ", + "removeConfirmTitle": "هدف لرې کړو؟", + "removeConfirmMessage": "CLI د دې غونډې هدف پاکوي.", + "updateFailed": "هدف نشو کولای تازه شي.", + "sectionAccessibility": "{{status}}. هدف: {{text}}" + }, "newSession": { "connectGithubDescription": "په خپل براوزر کې GitHub ونښلوئ، بیا دلته راشئ او یو زېرمتون غوره کړئ.", "openGithub": "GitHub پرانیزئ", diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index 9dfa824ff4..03cb94ad88 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -1542,6 +1542,22 @@ "connectionLost": "Conexão perdida", "retryConnection": "Tentar reconectar" }, + "goal": { + "title": "Meta", + "statusActive": "Meta ativa", + "statusPaused": "Meta pausada", + "statusComplete": "Meta concluída", + "statusBlocked": "Meta bloqueada", + "edit": "Editar meta", + "remove": "Remover meta", + "pause": "Pausar meta", + "resume": "Retomar meta", + "editPlaceholder": "Descreva a meta", + "removeConfirmTitle": "Remover meta?", + "removeConfirmMessage": "A CLI limpa a meta desta sessão.", + "updateFailed": "Não foi possível atualizar a meta.", + "sectionAccessibility": "{{status}}. Meta: {{text}}" + }, "newSession": { "connectGithubDescription": "Conecte o GitHub no seu navegador e volte aqui para escolher um repositório.", "openGithub": "Abrir GitHub", diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index 8ad1a0be9f..32ac4d20cb 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -2012,6 +2012,22 @@ "connectionLost": "Ligação perdida", "retryConnection": "Tentar ligar novamente" }, + "goal": { + "title": "Objetivo", + "statusActive": "Objetivo ativo", + "statusPaused": "Objetivo em pausa", + "statusComplete": "Objetivo concluído", + "statusBlocked": "Objetivo bloqueado", + "edit": "Editar objetivo", + "remove": "Remover objetivo", + "pause": "Colocar objetivo em pausa", + "resume": "Retomar objetivo", + "editPlaceholder": "Descreva o objetivo", + "removeConfirmTitle": "Remover objetivo?", + "removeConfirmMessage": "O CLI limpa o objetivo desta sessão.", + "updateFailed": "Não foi possível atualizar o objetivo.", + "sectionAccessibility": "{{status}}. Objetivo: {{text}}" + }, "newSession": { "connectGithubDescription": "Ligue a sua conta do GitHub no navegador e volte aqui para escolher um repositório.", "openGithub": "Abrir o GitHub", diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index 20615b1cbc..087e35179c 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -2012,6 +2012,22 @@ "connectionLost": "Conexiune pierdută", "retryConnection": "Încearcă din nou conectarea" }, + "goal": { + "title": "Obiectiv", + "statusActive": "Obiectiv activ", + "statusPaused": "Obiectiv în pauză", + "statusComplete": "Obiectiv finalizat", + "statusBlocked": "Obiectiv blocat", + "edit": "Editează obiectivul", + "remove": "Elimină obiectivul", + "pause": "Pune obiectivul pe pauză", + "resume": "Reia obiectivul", + "editPlaceholder": "Descrie obiectivul", + "removeConfirmTitle": "Elimini obiectivul?", + "removeConfirmMessage": "CLI șterge obiectivul acestei sesiuni.", + "updateFailed": "Obiectivul nu a putut fi actualizat.", + "sectionAccessibility": "{{status}}. Obiectiv: {{text}}" + }, "newSession": { "connectGithubDescription": "Conectează contul GitHub în browser, apoi revino aici pentru a alege un depozit.", "openGithub": "Deschide GitHub", diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index ae3522ea5b..132f704097 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -1580,6 +1580,22 @@ "connectionLost": "Соединение прервано", "retryConnection": "Повторить подключение" }, + "goal": { + "title": "Цель", + "statusActive": "Активная цель", + "statusPaused": "Цель на паузе", + "statusComplete": "Цель достигнута", + "statusBlocked": "Цель заблокирована", + "edit": "Изменить цель", + "remove": "Удалить цель", + "pause": "Приостановить цель", + "resume": "Возобновить цель", + "editPlaceholder": "Опишите цель", + "removeConfirmTitle": "Удалить цель?", + "removeConfirmMessage": "CLI очищает цель этой сессии.", + "updateFailed": "Не удалось обновить цель.", + "sectionAccessibility": "{{status}}. Цель: {{text}}" + }, "newSession": { "connectGithubDescription": "Подключите GitHub в браузере, затем вернитесь сюда, чтобы выбрать репозиторий.", "openGithub": "Открыть GitHub", diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index 5dcb8fa52f..fd924a27b0 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -1996,6 +1996,22 @@ "connectionLost": "සම්බන්ධතාව බිඳ වැටුණි", "retryConnection": "නැවත සම්බන්ධ වීමට උත්සාහ කරන්න" }, + "goal": { + "title": "ඉලක්කය", + "statusActive": "සක්‍රීය ඉලක්කය", + "statusPaused": "විරාමිත ඉලක්කය", + "statusComplete": "ඉලක්කය සම්පූර්ණයි", + "statusBlocked": "ඉලක්කය අවහිර කර ඇත", + "edit": "ඉලක්කය සංස්කරණය කරන්න", + "remove": "ඉලක්කය ඉවත් කරන්න", + "pause": "ඉලක්කය විරාම කරන්න", + "resume": "ඉලක්කය නැවත අරඹන්න", + "editPlaceholder": "ඉලක්කය විස්තර කරන්න", + "removeConfirmTitle": "ඉලක්කය ඉවත් කරන්නද?", + "removeConfirmMessage": "CLI මෙම සැසියේ ඉලක්කය මකා දමයි.", + "updateFailed": "ඉලක්කය යාවත්කාලීන කළ නොහැකි විය.", + "sectionAccessibility": "{{status}}. ඉලක්කය: {{text}}" + }, "newSession": { "connectGithubDescription": "ඔබේ බ්‍රවුසරයෙන් GitHub සම්බන්ධ කරන්න. ඉන්පසු ගබඩාවක් තෝරා ගැනීමට මෙහි ආපසු එන්න.", "openGithub": "GitHub විවෘත කරන්න", diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index f50c0b0352..057396178e 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -2028,6 +2028,22 @@ "connectionLost": "Pripojenie sa prerušilo", "retryConnection": "Znova sa pripojiť" }, + "goal": { + "title": "Cieľ", + "statusActive": "Aktívny cieľ", + "statusPaused": "Pozastavený cieľ", + "statusComplete": "Cieľ dokončený", + "statusBlocked": "Cieľ zablokovaný", + "edit": "Upraviť cieľ", + "remove": "Odstrániť cieľ", + "pause": "Pozastaviť cieľ", + "resume": "Obnoviť cieľ", + "editPlaceholder": "Opíšte cieľ", + "removeConfirmTitle": "Odstrániť cieľ?", + "removeConfirmMessage": "CLI vymaže cieľ tejto relácie.", + "updateFailed": "Cieľ sa nepodarilo aktualizovať.", + "sectionAccessibility": "{{status}}. Cieľ: {{text}}" + }, "newSession": { "connectGithubDescription": "Pripoj GitHub v prehliadači, potom sa sem vráť a vyber repozitár.", "openGithub": "Otvoriť GitHub", diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index f3ee83bbce..b4f14db2d8 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -2028,6 +2028,22 @@ "connectionLost": "Povezava prekinjena", "retryConnection": "Znova vzpostavi povezavo" }, + "goal": { + "title": "Cilj", + "statusActive": "Aktivni cilj", + "statusPaused": "Zaustavljeni cilj", + "statusComplete": "Cilj dokončan", + "statusBlocked": "Cilj blokiran", + "edit": "Uredi cilj", + "remove": "Odstrani cilj", + "pause": "Zaustavi cilj", + "resume": "Nadaljuj cilj", + "editPlaceholder": "Opišite cilj", + "removeConfirmTitle": "Odstranim cilj?", + "removeConfirmMessage": "CLI izbriše cilj te seje.", + "updateFailed": "Cilja ni bilo mogoče posodobiti.", + "sectionAccessibility": "{{status}}. Cilj: {{text}}" + }, "newSession": { "connectGithubDescription": "Poveži GitHub v brskalniku, nato se vrni sem in izberi skladišče.", "openGithub": "Odpri GitHub", diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index 7fbee7e922..b0b3d72d96 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -1996,6 +1996,22 @@ "connectionLost": "Xiriirku wuu go'ay", "retryConnection": "Mar kale isku day inaad ku xirmato" }, + "goal": { + "title": "Hadaf", + "statusActive": "Hadaf firfircoon", + "statusPaused": "Hadaf la hakiyay", + "statusComplete": "Hadafka waa dhammaaday", + "statusBlocked": "Hadafka waa la xiray", + "edit": "Tafatir hadafka", + "remove": "Ka saar hadafka", + "pause": "Hakiso hadafka", + "resume": "Sii wad hadafka", + "editPlaceholder": "Sharax hadafka", + "removeConfirmTitle": "Ka saar hadafka?", + "removeConfirmMessage": "CLI wuxuu tirtiraa hadafka kulankan.", + "updateFailed": "Hadafka lama cusboonaysiin karin.", + "sectionAccessibility": "{{status}}. Hadaf: {{text}}" + }, "newSession": { "connectGithubDescription": "GitHub ku xiriiri biraawsarkaaga, ka dibna halkan ku soo noqo si aad kayd u doorato.", "openGithub": "Fur GitHub", diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index 38f5fc3173..b9516e1be3 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -1996,6 +1996,22 @@ "connectionLost": "Lidhja u humb", "retryConnection": "Provo të lidhesh përsëri" }, + "goal": { + "title": "Qëllimi", + "statusActive": "Qëllim aktiv", + "statusPaused": "Qëllim i ndalur", + "statusComplete": "Qëllimi u përfundua", + "statusBlocked": "Qëllimi u bllokua", + "edit": "Redakto qëllimin", + "remove": "Hiq qëllimin", + "pause": "Ndalo qëllimin", + "resume": "Vazhdo qëllimin", + "editPlaceholder": "Përshkruaj qëllimin", + "removeConfirmTitle": "Të hiqet qëllimi?", + "removeConfirmMessage": "CLI fshin qëllimin e këtij sesioni.", + "updateFailed": "Qëllimi nuk mund të përditësohej.", + "sectionAccessibility": "{{status}}. Qëllim: {{text}}" + }, "newSession": { "connectGithubDescription": "Lidh llogarinë e GitHub në shfletuesin tënd, pastaj kthehu këtu për të zgjedhur një depo.", "openGithub": "Hap GitHub", diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index 5c6cbb80c4..419866f1c5 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -2012,6 +2012,22 @@ "connectionLost": "Veza je izgubljena", "retryConnection": "Pokušaj ponovo da se povežeš" }, + "goal": { + "title": "Cilj", + "statusActive": "Aktivan cilj", + "statusPaused": "Pauziran cilj", + "statusComplete": "Cilj završen", + "statusBlocked": "Cilj blokiran", + "edit": "Uredi cilj", + "remove": "Ukloni cilj", + "pause": "Pauziraj cilj", + "resume": "Nastavi cilj", + "editPlaceholder": "Opišite cilj", + "removeConfirmTitle": "Ukloniti cilj?", + "removeConfirmMessage": "CLI briše cilj ove sesije.", + "updateFailed": "Cilj nije moguće ažurirati.", + "sectionAccessibility": "{{status}}. Cilj: {{text}}" + }, "newSession": { "connectGithubDescription": "Poveži GitHub u pregledaču, a zatim se vrati ovde da izabereš repozitorijum.", "openGithub": "Otvori GitHub", diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index 40f30db602..55ab25e79d 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -1996,6 +1996,22 @@ "connectionLost": "Anslutningen bröts", "retryConnection": "Försök ansluta igen" }, + "goal": { + "title": "Mål", + "statusActive": "Aktivt mål", + "statusPaused": "Pausat mål", + "statusComplete": "Mål slutfört", + "statusBlocked": "Mål blockerat", + "edit": "Redigera mål", + "remove": "Ta bort mål", + "pause": "Pausa mål", + "resume": "Återuppta mål", + "editPlaceholder": "Beskriv målet", + "removeConfirmTitle": "Ta bort mål?", + "removeConfirmMessage": "CLI rensar den här sessionens mål.", + "updateFailed": "Målet kunde inte uppdateras.", + "sectionAccessibility": "{{status}}. Mål: {{text}}" + }, "newSession": { "connectGithubDescription": "Anslut GitHub i webbläsaren och kom sedan tillbaka hit för att välja ett kodarkiv.", "openGithub": "Öppna GitHub", diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index e5d4165035..33fbc763ca 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -1996,6 +1996,22 @@ "connectionLost": "Muunganisho umekatika", "retryConnection": "Jaribu kuunganisha tena" }, + "goal": { + "title": "Lengo", + "statusActive": "Lengo amilifu", + "statusPaused": "Lengo lililositishwa", + "statusComplete": "Lengo limekamilika", + "statusBlocked": "Lengo limezuiwa", + "edit": "Hariri lengo", + "remove": "Ondoa lengo", + "pause": "Sitisha lengo", + "resume": "Endeleza lengo", + "editPlaceholder": "Eleza lengo", + "removeConfirmTitle": "Ondoa lengo?", + "removeConfirmMessage": "CLI hufuta lengo la kikao hiki.", + "updateFailed": "Lengo halikuweza kusasishwa.", + "sectionAccessibility": "{{status}}. Lengo: {{text}}" + }, "newSession": { "connectGithubDescription": "Unganisha GitHub kwenye kivinjari chako, kisha rudi hapa kuchagua hazina.", "openGithub": "Fungua GitHub", diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index ca8616b9af..f239804070 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -1996,6 +1996,22 @@ "connectionLost": "இணைப்பு துண்டிக்கப்பட்டது", "retryConnection": "மீண்டும் இணைக்க முயற்சிக்கவும்" }, + "goal": { + "title": "இலக்கு", + "statusActive": "செயலில் உள்ள இலக்கு", + "statusPaused": "இடைநிறுத்தப்பட்ட இலக்கு", + "statusComplete": "இலக்கு நிறைவடைந்தது", + "statusBlocked": "இலக்கு தடுக்கப்பட்டது", + "edit": "இலக்கைத் திருத்து", + "remove": "இலக்கை அகற்று", + "pause": "இலக்கை இடைநிறுத்து", + "resume": "இலக்கை மீண்டும் தொடங்கு", + "editPlaceholder": "இலக்கை விவரிக்கவும்", + "removeConfirmTitle": "இலக்கை அகற்றவா?", + "removeConfirmMessage": "இந்த அமர்வின் இலக்கை CLI அழிக்கிறது.", + "updateFailed": "இலக்கைப் புதுப்பிக்க முடியவில்லை.", + "sectionAccessibility": "{{status}}. இலக்கு: {{text}}" + }, "newSession": { "connectGithubDescription": "உலாவியில் GitHub-ஐ இணைக்கவும். பின்னர் களஞ்சியத்தைத் தேர்ந்தெடுக்க இங்கே திரும்பவும்.", "openGithub": "GitHub-ஐத் திறக்கவும்", diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index add7304d58..67ad549dbb 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -1996,6 +1996,22 @@ "connectionLost": "కనెక్షన్ తెగిపోయింది", "retryConnection": "కనెక్ట్ చేయడానికి మళ్లీ ప్రయత్నించండి" }, + "goal": { + "title": "లక్ష్యం", + "statusActive": "క్రియాశీల లక్ష్యం", + "statusPaused": "నిలిపివేసిన లక్ష్యం", + "statusComplete": "లక్ష్యం పూర్తయింది", + "statusBlocked": "లక్ష్యం నిరోధించబడింది", + "edit": "లక్ష్యాన్ని సవరించండి", + "remove": "లక్ష్యాన్ని తీసివేయండి", + "pause": "లక్ష్యాన్ని నిలిపివేయండి", + "resume": "లక్ష్యాన్ని కొనసాగించండి", + "editPlaceholder": "లక్ష్యాన్ని వివరించండి", + "removeConfirmTitle": "లక్ష్యాన్ని తీసివేయాలా?", + "removeConfirmMessage": "ఈ సెషన్ లక్ష్యాన్ని CLI క్లియర్ చేస్తుంది.", + "updateFailed": "లక్ష్యాన్ని నవీకరించలేకపోయాము.", + "sectionAccessibility": "{{status}}. లక్ష్యం: {{text}}" + }, "newSession": { "connectGithubDescription": "మీ బ్రౌజర్‌లో GitHubను కనెక్ట్ చేయండి. తర్వాత రిపోజిటరీని ఎంచుకోవడానికి ఇక్కడికి తిరిగి రండి.", "openGithub": "GitHub తెరవండి", diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index 3347c65df4..f64d929836 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -1996,6 +1996,22 @@ "connectionLost": "การเชื่อมต่อขาดหาย", "retryConnection": "ลองเชื่อมต่อใหม่" }, + "goal": { + "title": "เป้าหมาย", + "statusActive": "เป้าหมายที่ใช้งานอยู่", + "statusPaused": "เป้าหมายที่หยุดชั่วคราว", + "statusComplete": "เป้าหมายเสร็จสมบูรณ์", + "statusBlocked": "เป้าหมายถูกบล็อก", + "edit": "แก้ไขเป้าหมาย", + "remove": "ลบเป้าหมาย", + "pause": "หยุดเป้าหมายชั่วคราว", + "resume": "ดำเนินการเป้าหมายต่อ", + "editPlaceholder": "อธิบายเป้าหมาย", + "removeConfirmTitle": "ลบเป้าหมายหรือไม่", + "removeConfirmMessage": "CLI จะล้างเป้าหมายของเซสชันนี้", + "updateFailed": "ไม่สามารถอัปเดตเป้าหมายได้", + "sectionAccessibility": "{{status}} เป้าหมาย: {{text}}" + }, "newSession": { "connectGithubDescription": "เชื่อมต่อ GitHub ในเบราว์เซอร์ แล้วกลับมาเลือกคลังโค้ดที่นี่", "openGithub": "เปิด GitHub", diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index 078d5313b2..731ba9f66e 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -1562,6 +1562,22 @@ "connectionLost": "Bağlantı koptu", "retryConnection": "Yeniden bağlanmayı dene" }, + "goal": { + "title": "Hedef", + "statusActive": "Etkin hedef", + "statusPaused": "Duraklatılmış hedef", + "statusComplete": "Hedef tamamlandı", + "statusBlocked": "Hedef engellendi", + "edit": "Hedefi düzenle", + "remove": "Hedefi kaldır", + "pause": "Hedefi duraklat", + "resume": "Hedefe devam et", + "editPlaceholder": "Hedefi açıklayın", + "removeConfirmTitle": "Hedef kaldırılsın mı?", + "removeConfirmMessage": "CLI bu oturumun hedefini temizler.", + "updateFailed": "Hedef güncellenemedi.", + "sectionAccessibility": "{{status}}. Hedef: {{text}}" + }, "newSession": { "connectGithubDescription": "Tarayıcında GitHub hesabını bağla, ardından bir depo seçmek için buraya dön.", "openGithub": "GitHub'ı aç", diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index 981d7ac246..45e9923025 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -1580,6 +1580,22 @@ "connectionLost": "З'єднання втрачено", "retryConnection": "Повторити підключення" }, + "goal": { + "title": "Ціль", + "statusActive": "Активна ціль", + "statusPaused": "Призупинена ціль", + "statusComplete": "Ціль досягнуто", + "statusBlocked": "Ціль заблоковано", + "edit": "Редагувати ціль", + "remove": "Видалити ціль", + "pause": "Призупинити ціль", + "resume": "Відновити ціль", + "editPlaceholder": "Опишіть ціль", + "removeConfirmTitle": "Видалити ціль?", + "removeConfirmMessage": "CLI очищає ціль цього сеансу.", + "updateFailed": "Не вдалося оновити ціль.", + "sectionAccessibility": "{{status}}. Ціль: {{text}}" + }, "newSession": { "connectGithubDescription": "Підключіть GitHub у браузері, а потім поверніться сюди, щоб вибрати репозиторій.", "openGithub": "Відкрити GitHub", diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index bd68847183..cdcd7b32cd 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -1996,6 +1996,22 @@ "connectionLost": "کنکشن ٹوٹ گیا", "retryConnection": "کنکشن دوبارہ قائم کرنے کی کوشش کریں" }, + "goal": { + "title": "ہدف", + "statusActive": "فعال ہدف", + "statusPaused": "روکا گیا ہدف", + "statusComplete": "ہدف مکمل ہو گیا", + "statusBlocked": "ہدف مسدود ہے", + "edit": "ہدف میں ترمیم کریں", + "remove": "ہدف ہٹائیں", + "pause": "ہدف روکیں", + "resume": "ہدف دوبارہ شروع کریں", + "editPlaceholder": "ہدف بیان کریں", + "removeConfirmTitle": "ہدف ہٹائیں؟", + "removeConfirmMessage": "CLI اس سیشن کا ہدف صاف کر دیتا ہے۔", + "updateFailed": "ہدف اپ ڈیٹ نہیں ہو سکا۔", + "sectionAccessibility": "{{status}}. ہدف: {{text}}" + }, "newSession": { "connectGithubDescription": "اپنے براؤزر میں GitHub کو منسلک کریں، پھر ریپوزٹری منتخب کرنے کے لیے یہاں واپس آئیں۔", "openGithub": "GitHub کھولیں", diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index dd9634939b..382d94e092 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -1996,6 +1996,22 @@ "connectionLost": "Ulanish uzildi", "retryConnection": "Qayta ulanish" }, + "goal": { + "title": "Maqsad", + "statusActive": "Faol maqsad", + "statusPaused": "To'xtatilgan maqsad", + "statusComplete": "Maqsad bajarildi", + "statusBlocked": "Maqsad bloklandi", + "edit": "Maqsadni tahrirlash", + "remove": "Maqsadni olib tashlash", + "pause": "Maqsadni to'xtatish", + "resume": "Maqsadni davom ettirish", + "editPlaceholder": "Maqsadni tavsiflang", + "removeConfirmTitle": "Maqsad olib tashlansinmi?", + "removeConfirmMessage": "CLI bu seansning maqsadini tozalaydi.", + "updateFailed": "Maqsadni yangilab bo'lmadi.", + "sectionAccessibility": "{{status}}. Maqsad: {{text}}" + }, "newSession": { "connectGithubDescription": "Brauzerda GitHub hisobingizni ulang, so'ng repozitoriya tanlash uchun bu yerga qayting.", "openGithub": "GitHub saytini ochish", diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index 9e5aa9967e..5748ce5d13 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -1562,6 +1562,22 @@ "connectionLost": "Mất kết nối", "retryConnection": "Thử kết nối lại" }, + "goal": { + "title": "Mục tiêu", + "statusActive": "Mục tiêu đang hoạt động", + "statusPaused": "Mục tiêu tạm dừng", + "statusComplete": "Mục tiêu đã hoàn thành", + "statusBlocked": "Mục tiêu bị chặn", + "edit": "Chỉnh sửa mục tiêu", + "remove": "Xóa mục tiêu", + "pause": "Tạm dừng mục tiêu", + "resume": "Tiếp tục mục tiêu", + "editPlaceholder": "Mô tả mục tiêu", + "removeConfirmTitle": "Xóa mục tiêu?", + "removeConfirmMessage": "CLI xóa mục tiêu của phiên này.", + "updateFailed": "Không thể cập nhật mục tiêu.", + "sectionAccessibility": "{{status}}. Mục tiêu: {{text}}" + }, "newSession": { "connectGithubDescription": "Kết nối GitHub trong trình duyệt, rồi quay lại đây để chọn kho mã.", "openGithub": "Mở GitHub", diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index d35b952a10..708d396c5d 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -1996,6 +1996,22 @@ "connectionLost": "Àsopọ̀ ti já", "retryConnection": "Gbìyànjú láti tún sopọ̀" }, + "goal": { + "title": "Àfojúsùn", + "statusActive": "Àfojúsùn tí ń ṣiṣẹ́", + "statusPaused": "Àfojúsùn tí a dá dúró", + "statusComplete": "Àfojúsùn ti parí", + "statusBlocked": "Àfojúsùn ti dí", + "edit": "Ṣàtúnṣe àfojúsùn", + "remove": "Yọ àfojúsùn kuro", + "pause": "Dá àfojúsùn dúró", + "resume": "Tẹ̀síwájú àfojúsùn", + "editPlaceholder": "Ṣàpèjúwe àfojúsùn", + "removeConfirmTitle": "Yọ àfojúsùn kuro?", + "removeConfirmMessage": "CLI ń pa àfojúsùn ìpàdé yìí rẹ́.", + "updateFailed": "A kò lè ṣàtúnṣe àfojúsùn.", + "sectionAccessibility": "{{status}}. Àfojúsùn: {{text}}" + }, "newSession": { "connectGithubDescription": "So GitHub pọ̀ nínú aṣàwákiri rẹ, lẹ́yìn náà padà síbí láti yan ibi ìpamọ́ kóòdù.", "openGithub": "Ṣí GitHub", diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index 5de03ef03d..769b0217cd 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -1562,6 +1562,22 @@ "connectionLost": "连接已断开", "retryConnection": "重新连接" }, + "goal": { + "title": "目标", + "statusActive": "进行中的目标", + "statusPaused": "已暂停的目标", + "statusComplete": "目标已完成", + "statusBlocked": "目标已阻塞", + "edit": "编辑目标", + "remove": "移除目标", + "pause": "暂停目标", + "resume": "继续目标", + "editPlaceholder": "描述目标", + "removeConfirmTitle": "移除目标?", + "removeConfirmMessage": "CLI 会清除此会话的目标。", + "updateFailed": "无法更新目标。", + "sectionAccessibility": "{{status}}。目标:{{text}}" + }, "newSession": { "connectGithubDescription": "在浏览器中连接 GitHub,然后返回此处选择仓库。", "openGithub": "打开 GitHub", diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index 0618078679..f7fbe7a48f 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -1562,6 +1562,22 @@ "connectionLost": "連線中斷", "retryConnection": "重新連線" }, + "goal": { + "title": "目標", + "statusActive": "進行中的目標", + "statusPaused": "已暫停的目標", + "statusComplete": "目標已完成", + "statusBlocked": "目標已封鎖", + "edit": "編輯目標", + "remove": "移除目標", + "pause": "暫停目標", + "resume": "繼續目標", + "editPlaceholder": "描述目標", + "removeConfirmTitle": "移除目標?", + "removeConfirmMessage": "CLI 會清除此工作階段的目標。", + "updateFailed": "無法更新目標。", + "sectionAccessibility": "{{status}}。目標:{{text}}" + }, "newSession": { "connectGithubDescription": "請在瀏覽器中連結 GitHub,再回到這裡選取儲存庫。", "openGithub": "開啟 GitHub", diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index f9c3d3a210..70c345dead 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -1996,6 +1996,22 @@ "connectionLost": "Ukuxhumana kunqamukile", "retryConnection": "Zama ukuxhuma futhi" }, + "goal": { + "title": "Umgomo", + "statusActive": "Umgomo osebenzayo", + "statusPaused": "Umgomo omisiwe", + "statusComplete": "Umgomo uqediwe", + "statusBlocked": "Umgomo uvinjiwe", + "edit": "Hlela umgomo", + "remove": "Susa umgomo", + "pause": "Misa umgomo", + "resume": "Qhubeka nomgomo", + "editPlaceholder": "Chaza umgomo", + "removeConfirmTitle": "Susa umgomo?", + "removeConfirmMessage": "I-CLI isusa umgomo wale seshini.", + "updateFailed": "Ayikwazanga ukubuyekeza umgomo.", + "sectionAccessibility": "{{status}}. Umgomo: {{text}}" + }, "newSession": { "connectGithubDescription": "Xhuma i-GitHub esipheqululini sakho, bese ubuyela lapha ukuze ukhethe inqolobane yekhodi.", "openGithub": "Vula i-GitHub", diff --git a/apps/web/src/lib/session-ingest-client.ts b/apps/web/src/lib/session-ingest-client.ts index 13c2acff27..7e67fe99cc 100644 --- a/apps/web/src/lib/session-ingest-client.ts +++ b/apps/web/src/lib/session-ingest-client.ts @@ -125,6 +125,9 @@ const SessionMessagesPageResponseSchema = z.object({ success: z.literal(true), kiloSessionId: z.string().min(1), history: kiloSdkMessageHistorySchema.nullable(), + // Session-level metadata (records keyed by `kilo.`, e.g. + // `kilo.goal`). Optional so an older worker response stays parseable. + sessionMetadata: z.record(z.string(), z.unknown()).nullable().optional(), }); export type SessionMessagesPageOptions = { @@ -137,6 +140,8 @@ export type SessionMessagesPageOptions = { export type SessionMessagesPageResult = { kiloSessionId: string; history: KiloSdkMessageHistory | null; + /** Session-level metadata from the ingest snapshot, when available. */ + sessionMetadata?: Record | null; watermarkEventId?: number | null; }; @@ -209,7 +214,13 @@ export async function fetchSessionMessagesPage( throw error; } - return { kiloSessionId: parsed.data.kiloSessionId, history: parsed.data.history }; + return { + kiloSessionId: parsed.data.kiloSessionId, + history: parsed.data.history, + ...(parsed.data.sessionMetadata === undefined + ? {} + : { sessionMetadata: parsed.data.sessionMetadata }), + }; } // --------------------------------------------------------------------------- diff --git a/apps/web/src/routers/cli-sessions-v2-router.test.ts b/apps/web/src/routers/cli-sessions-v2-router.test.ts index 6a8c16d009..f7bd3dd484 100644 --- a/apps/web/src/routers/cli-sessions-v2-router.test.ts +++ b/apps/web/src/routers/cli-sessions-v2-router.test.ts @@ -738,6 +738,29 @@ describe('cli-sessions-v2-router', () => { }); }); + it('projects the session goal from the snapshot metadata and never leaks the raw metadata', async () => { + const goal = { text: 'Ship p7 objective', status: 'paused' as const }; + fetchSessionMessagesPage.mockResolvedValueOnce({ + kiloSessionId: sessionId, + history: { messages: [], nextCursor: null, omittedItemCount: 0 }, + sessionMetadata: { 'kilo.goal': goal, 'kilo.somethingPrivate': '/srv/secret' }, + }); + + const caller = await createCallerForUser(regularUser.id); + const result = await caller.cliSessionsV2.getSessionMessagesPage({ + session_id: sessionId, + limit: 50, + }); + + expect(result).toEqual({ + kiloSessionId: sessionId, + history: { messages: [], nextCursor: null, omittedItemCount: 0 }, + watermarkEventId: null, + sessionGoal: goal, + }); + expect(result).not.toHaveProperty('sessionMetadata'); + }); + it('preserves retryable_failure so the UI can offer Retry', async () => { const history = { kind: 'retryable_failure' as const, diff --git a/apps/web/src/routers/cli-sessions-v2-router.ts b/apps/web/src/routers/cli-sessions-v2-router.ts index 49451813da..d26698f9ce 100644 --- a/apps/web/src/routers/cli-sessions-v2-router.ts +++ b/apps/web/src/routers/cli-sessions-v2-router.ts @@ -46,6 +46,7 @@ import { validateKiloSdkMessagesCursor, } from '@kilocode/session-ingest-contracts'; import { baseGetSessionNextOutputSchema } from './cloud-agent-next-schemas'; +import { projectSessionGoal } from '@kilocode/cloud-agent-sdk'; import { KNOWN_PLATFORMS } from '@kilocode/app-shared/platforms'; import { verifyWebhookTriggerAccess } from '@/lib/webhook-trigger-ownership'; import { ensureOrganizationAccess } from '@/routers/organizations/utils'; @@ -1484,17 +1485,25 @@ export const cliSessionsV2Router = createTRPCRouter({ }); } + // Project the session goal from the snapshot metadata so clients can + // restore the fixed goal section on open and on reconnect. The raw + // session metadata stays server-side; only the validated goal is + // returned, so no private CLI metadata leaks to the client. + const { sessionMetadata, ...pageResult } = result; + const sessionGoal = projectSessionGoal(sessionMetadata); + return { ...(session.cloud_agent_worktree_id ? projectGroupedSessionTranscript( - result, + pageResult, input.session_id, - result.history && 'messages' in result.history - ? result.history.messages.map(message => message.info) + pageResult.history && 'messages' in pageResult.history + ? pageResult.history.messages.map(message => message.info) : [] ) - : result), + : pageResult), watermarkEventId, + ...(sessionGoal === undefined ? {} : { sessionGoal }), }; }), diff --git a/packages/cloud-agent-sdk/src/index.ts b/packages/cloud-agent-sdk/src/index.ts index 0cb38b9db4..d8073a5278 100644 --- a/packages/cloud-agent-sdk/src/index.ts +++ b/packages/cloud-agent-sdk/src/index.ts @@ -39,7 +39,7 @@ export type { PermissionResponse, } from './session'; -export { normalize, normalizeCliEvent, isChatEvent } from './normalizer'; +export { normalize, normalizeCliEvent, isChatEvent, projectSessionGoal } from './normalizer'; export type { NormalizedEvent, ChatEvent, ServiceEvent } from './normalizer'; export { reduce } from './reducer'; @@ -167,6 +167,8 @@ export type { PreparationStepStatus, ServiceStateSnapshot, SessionInfo, + SessionGoal, + SessionGoalStatus, KiloSessionId, CloudAgentSessionId, ResolvedSession, diff --git a/packages/cloud-agent-sdk/src/normalizer.test.ts b/packages/cloud-agent-sdk/src/normalizer.test.ts index 24cec70b33..4b4f372b38 100644 --- a/packages/cloud-agent-sdk/src/normalizer.test.ts +++ b/packages/cloud-agent-sdk/src/normalizer.test.ts @@ -551,6 +551,137 @@ describe('normalize', () => { it('returns null when info.id is missing', () => { expect(normalize(createRaw('session.updated', { info: { title: 'No ID' } }))).toBeNull(); }); + + it('projects a valid goal with a reason from metadata[kilo.goal]', () => { + const result = normalizeCliEvent('session.updated', { + info: { + id: 'ses-1', + metadata: { + 'kilo.goal': { text: 'Ship the slice', status: 'active', reason: 'In review' }, + }, + }, + }); + + expect(result).toEqual({ + type: 'session.updated', + info: { + id: 'ses-1', + parentID: undefined, + goal: { text: 'Ship the slice', status: 'active', reason: 'In review' }, + }, + }); + }); + + it('projects a goal from nested metadata.kilo.goal', () => { + const result = normalizeCliEvent('session.updated', { + info: { + id: 'ses-1', + metadata: { kilo: { goal: { text: 'Nested goal', status: 'paused' } } }, + }, + }); + + expect(result).toEqual({ + type: 'session.updated', + info: { + id: 'ses-1', + parentID: undefined, + goal: { text: 'Nested goal', status: 'paused' }, + }, + }); + }); + + it('projects every goal status value', () => { + const statuses = ['active', 'complete', 'blocked', 'paused'] as const; + for (const status of statuses) { + const result = normalizeCliEvent('session.updated', { + info: { id: 'ses-1', metadata: { 'kilo.goal': { text: 'Goal', status } } }, + }); + expect(result).toEqual({ + type: 'session.updated', + info: { id: 'ses-1', parentID: undefined, goal: { text: 'Goal', status } }, + }); + } + }); + + it('drops a goal with an unknown status', () => { + const result = normalizeCliEvent('session.updated', { + info: { + id: 'ses-1', + metadata: { 'kilo.goal': { text: 'Goal', status: 'done' } }, + }, + }); + + expect(result).toEqual({ + type: 'session.updated', + info: { id: 'ses-1', parentID: undefined }, + }); + }); + + it('drops a goal with empty or missing text', () => { + const empty = normalizeCliEvent('session.updated', { + info: { id: 'ses-1', metadata: { 'kilo.goal': { text: '', status: 'active' } } }, + }); + const missing = normalizeCliEvent('session.updated', { + info: { id: 'ses-1', metadata: { 'kilo.goal': { status: 'active' } } }, + }); + + expect(empty).toEqual({ + type: 'session.updated', + info: { id: 'ses-1', parentID: undefined }, + }); + expect(missing).toEqual({ + type: 'session.updated', + info: { id: 'ses-1', parentID: undefined }, + }); + }); + + it('keeps the goal key absent when metadata has no goal', () => { + const result = normalizeCliEvent('session.updated', { + info: { id: 'ses-1', metadata: { other: true } }, + }); + + expect(result).toEqual({ + type: 'session.updated', + info: { id: 'ses-1', parentID: undefined }, + }); + expect((result as { info: Record }).info).not.toHaveProperty('goal'); + }); + + it('does not throw on malformed metadata and drops the goal', () => { + const cases: Array | null | string | number> = [ + { 'kilo.goal': null }, + { 'kilo.goal': 'not-an-object' }, + { kilo: null }, + { kilo: 'not-an-object' }, + null, + 'metadata-string', + 42, + ]; + + for (const metadata of cases) { + const result = normalizeCliEvent('session.updated', { + info: { id: 'ses-1', metadata }, + }); + expect(result).toEqual({ + type: 'session.updated', + info: { id: 'ses-1', parentID: undefined }, + }); + } + }); + + it('drops an empty reason while keeping the goal', () => { + const result = normalizeCliEvent('session.updated', { + info: { + id: 'ses-1', + metadata: { 'kilo.goal': { text: 'Goal', status: 'active', reason: '' } }, + }, + }); + + expect(result).toEqual({ + type: 'session.updated', + info: { id: 'ses-1', parentID: undefined, goal: { text: 'Goal', status: 'active' } }, + }); + }); }); describe('session.error', () => { diff --git a/packages/cloud-agent-sdk/src/normalizer.ts b/packages/cloud-agent-sdk/src/normalizer.ts index bbe9d81d6a..fe2c9446e5 100644 --- a/packages/cloud-agent-sdk/src/normalizer.ts +++ b/packages/cloud-agent-sdk/src/normalizer.ts @@ -7,6 +7,8 @@ import { z } from 'zod'; import type { Part, SessionStatus, QuestionInfo, Message } from '@kilocode/app-shared/opencode'; import type { SessionInfo, + SessionGoal, + SessionGoalStatus, CloudStatus, SuggestionAction, SlashCommandInfo, @@ -241,6 +243,52 @@ const sessionModelSchema = z.object({ variant: z.string().optional(), }); +const SESSION_GOAL_STATUSES = new Set([ + 'active', + 'complete', + 'blocked', + 'paused', +]); + +/** Validate a candidate `kilo.goal` value. Returns undefined when malformed. */ +function parseSessionGoal(raw: unknown): SessionGoal | undefined { + if (typeof raw !== 'object' || raw === null) return undefined; + const goal = raw as Record; + const text = goal['text']; + if (typeof text !== 'string' || text.length === 0) return undefined; + const status = goal['status']; + if (typeof status !== 'string' || !SESSION_GOAL_STATUSES.has(status as SessionGoalStatus)) { + return undefined; + } + const reasonRaw = goal['reason']; + const reason = typeof reasonRaw === 'string' && reasonRaw.length > 0 ? reasonRaw : undefined; + return { + text, + status: status as SessionGoalStatus, + ...(reason === undefined ? {} : { reason }), + }; +} + +/** + * Project the CLI session goal from `session.info.metadata`. The CLI stores it + * under the dotted key `kilo.goal`; accept a nested `kilo.goal` object too. + * Malformed metadata drops the goal without throwing. + */ +export function projectSessionGoal(rawMetadata: unknown): SessionGoal | undefined { + if (typeof rawMetadata !== 'object' || rawMetadata === null) return undefined; + const metadata = rawMetadata as Record; + const candidates: unknown[] = [metadata['kilo.goal']]; + const kilo = metadata['kilo']; + if (typeof kilo === 'object' && kilo !== null) { + candidates.push((kilo as Record)['goal']); + } + for (const candidate of candidates) { + const goal = parseSessionGoal(candidate); + if (goal) return goal; + } + return undefined; +} + const connectedServiceDataSchema = connectedDataSchema.extend({ activeMessageId: z.string().nullable().optional().catch(undefined), }); @@ -255,10 +303,12 @@ const cloudMessageCanceledDataSchema = z function normalizeSessionInfo(rawInfo: { id: string; [key: string]: unknown }): SessionInfo { const model = sessionModelSchema.safeParse(rawInfo['model']); + const goal = projectSessionGoal(rawInfo['metadata']); return { id: rawInfo.id, parentID: rawInfo['parentID'] != null ? String(rawInfo['parentID']) : undefined, ...(model.success ? { model: model.data } : {}), + ...(goal === undefined ? {} : { goal }), }; } diff --git a/packages/cloud-agent-sdk/src/service-state.test.ts b/packages/cloud-agent-sdk/src/service-state.test.ts index f3134bb3de..bd29d3cbf4 100644 --- a/packages/cloud-agent-sdk/src/service-state.test.ts +++ b/packages/cloud-agent-sdk/src/service-state.test.ts @@ -1,6 +1,7 @@ import { createServiceState } from './service-state'; import type { ServiceStateConfig } from './service-state'; import type { Session, QuestionInfo } from '@kilocode/app-shared/opencode'; +import type { SessionGoal } from './types'; function makeConfig(overrides?: Partial): ServiceStateConfig { return { rootSessionId: 'root-1', ...overrides }; @@ -457,6 +458,55 @@ describe('createServiceState', () => { }); }); + describe('goal reason preservation', () => { + const completeGoal: SessionGoal = { + text: 'Report the goal finished with a reason', + status: 'complete', + reason: 'Reported by the working model, not independently verified.', + }; + + it('keeps a known reason when a later update for the same goal omits it', () => { + const state = createServiceState(makeConfig()); + state.process({ type: 'session.created', info: { id: 'root-1', goal: completeGoal } }); + state.process({ + type: 'session.updated', + info: { id: 'root-1', goal: { text: completeGoal.text, status: 'complete' } }, + }); + + expect(state.getSessionInfo()?.goal).toEqual(completeGoal); + }); + + it('drops the reason when the goal status changes (resume)', () => { + const state = createServiceState(makeConfig()); + state.process({ type: 'session.created', info: { id: 'root-1', goal: completeGoal } }); + state.process({ + type: 'session.updated', + info: { id: 'root-1', goal: { text: completeGoal.text, status: 'active' } }, + }); + + expect(state.getSessionInfo()?.goal).toEqual({ text: completeGoal.text, status: 'active' }); + }); + + it('drops the reason when the goal text changes (edit)', () => { + const state = createServiceState(makeConfig()); + state.process({ type: 'session.created', info: { id: 'root-1', goal: completeGoal } }); + state.process({ + type: 'session.updated', + info: { id: 'root-1', goal: { text: 'New objective', status: 'complete' } }, + }); + + expect(state.getSessionInfo()?.goal).toEqual({ text: 'New objective', status: 'complete' }); + }); + + it('does not carry a reason across a different session', () => { + const state = createServiceState(makeConfig()); + state.process({ type: 'session.created', info: { id: 'root-1', goal: completeGoal } }); + state.process({ type: 'session.created', info: { id: 'root-2' } }); + + expect(state.getSessionInfo()).toEqual({ id: 'root-2' }); + }); + }); + describe('question.asked', () => { it('sets question state and fires callback', () => { const onQuestionAsked = jest.fn(); diff --git a/packages/cloud-agent-sdk/src/service-state.ts b/packages/cloud-agent-sdk/src/service-state.ts index e7615025b4..2ce204663a 100644 --- a/packages/cloud-agent-sdk/src/service-state.ts +++ b/packages/cloud-agent-sdk/src/service-state.ts @@ -22,6 +22,24 @@ import type { PreparationStepSnapshot, } from './types'; +/** + * Keep a known goal reason when a later update for the same session carries the + * same goal without one. The CLI reports a terminal goal (complete/blocked) + * with a reason, but a snapshot replay or a partial event can omit it; without + * this, replacing the session info would drop copy the metadata already + * provided. Text and status must match, so a resume (status change) or an edit + * (text change) still clears the reason. + */ +function preserveGoalReason(previous: SessionInfo | null, next: SessionInfo): SessionInfo { + const nextGoal = next.goal; + const previousGoal = previous?.goal; + if (nextGoal === undefined || previousGoal === undefined) return next; + if (previous?.id !== next.id) return next; + if (nextGoal.reason !== undefined || previousGoal.reason === undefined) return next; + if (nextGoal.text !== previousGoal.text || nextGoal.status !== previousGoal.status) return next; + return { ...next, goal: { ...nextGoal, reason: previousGoal.reason } }; +} + type ServiceStateConfig = { /** The root session ID we're tracking (to detect child sessions). */ rootSessionId: string; @@ -254,18 +272,22 @@ function createServiceState(config: ServiceStateConfig): ServiceState { rootSessionId = event.info.id; } // Only track root session info + let info = event.info; if (isRootSession(event.info.id)) { - sessionInfo = event.info; + info = preserveGoalReason(sessionInfo, event.info); + sessionInfo = info; } - config.onSessionCreated?.(event.info); + config.onSessionCreated?.(info); notify(); } function processSessionUpdated(event: Extract): void { + let info = event.info; if (isRootSession(event.info.id)) { - sessionInfo = event.info; + info = preserveGoalReason(sessionInfo, event.info); + sessionInfo = info; } - config.onSessionUpdated?.(event.info); + config.onSessionUpdated?.(info); notify(); } diff --git a/packages/cloud-agent-sdk/src/types.ts b/packages/cloud-agent-sdk/src/types.ts index 84c2034c18..2865477e0b 100644 --- a/packages/cloud-agent-sdk/src/types.ts +++ b/packages/cloud-agent-sdk/src/types.ts @@ -64,6 +64,16 @@ export type ProcessedMessage = { parts: Part[]; }; +/** Goal status reported by the CLI in session metadata. */ +export type SessionGoalStatus = 'active' | 'complete' | 'blocked' | 'paused'; + +/** Session goal projected from CLI metadata under `kilo.goal`. */ +export type SessionGoal = { + text: string; + status: SessionGoalStatus; + reason?: string | undefined; +}; + /** Minimal session metadata — only the fields the SDK actually reads. */ export type SessionInfo = { id: string; @@ -75,6 +85,7 @@ export type SessionInfo = { variant?: string | undefined; } | undefined; + goal?: SessionGoal | undefined; }; export type SessionPhase = diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 71df83c789..e46da35717 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -16,6 +16,7 @@ } }, "dependencies": { + "@kilocode/cloud-agent-sdk": "workspace:*", "@kilocode/kilo-chat": "workspace:*", "@kilocode/kiloclaw-instance-tiers": "workspace:*", "@kilocode/kiloclaw-secret-catalog": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31b88e6f15..3070045f18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1654,6 +1654,9 @@ importers: packages/trpc: dependencies: + '@kilocode/cloud-agent-sdk': + specifier: workspace:* + version: link:../cloud-agent-sdk '@kilocode/kilo-chat': specifier: workspace:* version: link:../kilo-chat diff --git a/services/session-ingest/src/routes/api.test.ts b/services/session-ingest/src/routes/api.test.ts index e0d6886a7c..89d46b37de 100644 --- a/services/session-ingest/src/routes/api.test.ts +++ b/services/session-ingest/src/routes/api.test.ts @@ -1376,6 +1376,77 @@ describe('api routes', () => { expect(readKiloSdkMessages).toHaveBeenCalledWith({ limit: 50, before: undefined }); }); + it('GET /session/:sessionId/messages forwards the ingest snapshot metadata alongside the page', async () => { + const { db, fns } = makeDbFakes(); + vi.mocked(getWorkerDb).mockReturnValue(db); + fns.selectResult.mockResolvedValueOnce([{ session_id: 'ses_12345678901234567890123456' }]); + + const sessionMetadata = { 'kilo.goal': { text: 'Ship p7 objective', status: 'paused' } }; + const readKiloSdkMessages = vi.fn(async () => ({ + messages: [], + nextCursor: null, + omittedItemCount: 0, + })); + const readKiloSdkSessionSnapshot = vi.fn(async () => ({ + kind: 'value', + info: { id: 'ses_12345678901234567890123456', metadata: sessionMetadata }, + byteLength: 64, + })); + vi.mocked(getSessionIngestDO).mockReturnValue({ + readKiloSdkMessages, + readKiloSdkSessionSnapshot, + } as never); + + const app = makeApiApp(); + const res = await app.fetch( + new Request('http://local/session/ses_12345678901234567890123456/messages?limit=50', { + method: 'GET', + }), + makeTestEnv() + ); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toMatchObject({ + success: true, + kiloSessionId: 'ses_12345678901234567890123456', + sessionMetadata, + }); + }); + + it('GET /session/:sessionId/messages degrades to null metadata when the snapshot read fails', async () => { + const { db, fns } = makeDbFakes(); + vi.mocked(getWorkerDb).mockReturnValue(db); + fns.selectResult.mockResolvedValueOnce([{ session_id: 'ses_12345678901234567890123456' }]); + + const readKiloSdkMessages = vi.fn(async () => ({ + messages: [], + nextCursor: null, + omittedItemCount: 0, + })); + const readKiloSdkSessionSnapshot = vi.fn(async () => { + throw new Error('snapshot unavailable'); + }); + vi.mocked(getSessionIngestDO).mockReturnValue({ + readKiloSdkMessages, + readKiloSdkSessionSnapshot, + } as never); + + const app = makeApiApp(); + const res = await app.fetch( + new Request('http://local/session/ses_12345678901234567890123456/messages?limit=50', { + method: 'GET', + }), + makeTestEnv() + ); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body).toMatchObject({ + success: true, + kiloSessionId: 'ses_12345678901234567890123456', + sessionMetadata: null, + }); + }); + it('GET /session/:sessionId/messages defaults an omitted limit to the shared page size', async () => { const { db, fns } = makeDbFakes(); vi.mocked(getWorkerDb).mockReturnValue(db); diff --git a/services/session-ingest/src/routes/api.ts b/services/session-ingest/src/routes/api.ts index 159bb636a6..dc49b35df8 100644 --- a/services/session-ingest/src/routes/api.ts +++ b/services/session-ingest/src/routes/api.ts @@ -588,11 +588,52 @@ api.get('/session/:sessionId/messages', async c => { ); const history = persistedKiloSdkMessageHistorySchema.nullable().safeParse(rawHistory); + // Session-level state travels with the bounded message page so a client can + // restore it on open and on reconnect without a second snapshot round trip. + // The CLI goal lives in `session.info.metadata`, so only the metadata is + // forwarded (never the whole session info). Only the initial page (no + // cursor) carries it: older-message pages never rebuild the session header. + // Best-effort: a snapshot read failure must not fail the message page; the + // live session events still carry goal updates. + let sessionMetadata: Record | null = null; + if (inputParse.data.before === undefined) { + try { + const rawSessionInfo = await withDORetry, unknown>( + () => + getSessionIngestDO(c.env, { + kiloUserId, + sessionId: inputParse.data.kiloSessionId, + }), + stub => stub.readKiloSdkSessionSnapshot(), + 'SessionIngestDO.readKiloSdkSessionSnapshot' + ); + if ( + typeof rawSessionInfo === 'object' && + rawSessionInfo !== null && + (rawSessionInfo as { kind?: unknown }).kind === 'value' + ) { + const info = (rawSessionInfo as { info?: unknown }).info; + if (typeof info === 'object' && info !== null) { + const metadata = (info as { metadata?: unknown }).metadata; + if (typeof metadata === 'object' && metadata !== null && !Array.isArray(metadata)) { + sessionMetadata = metadata as Record; + } + } + } + } catch (error) { + console.warn( + `Failed to fetch session snapshot for session ${inputParse.data.kiloSessionId}:`, + error instanceof Error ? error.message : error + ); + } + } + return c.json( { success: true, kiloSessionId: inputParse.data.kiloSessionId, history: history.success ? history.data : { kind: 'invalid_data' }, + sessionMetadata, }, 200 );