@namzu/cli 30.0.0 → 33.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +261 -1
- package/dist/browser/commands.js +11 -7
- package/dist/browser/commands.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts +20 -2
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +42 -5
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/archive.d.ts +4 -0
- package/dist/commands/archive.d.ts.map +1 -0
- package/dist/commands/archive.js +62 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +2 -1
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/exec-json.d.ts.map +1 -1
- package/dist/commands/exec-json.js +24 -1
- package/dist/commands/exec-json.js.map +1 -1
- package/dist/commands/exec.d.ts.map +1 -1
- package/dist/commands/exec.js +2 -1
- package/dist/commands/exec.js.map +1 -1
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +33 -11
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/mcp-oauth-login.d.ts +19 -0
- package/dist/commands/mcp-oauth-login.d.ts.map +1 -0
- package/dist/commands/mcp-oauth-login.js +213 -0
- package/dist/commands/mcp-oauth-login.js.map +1 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +288 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/prior-messages.d.ts.map +1 -1
- package/dist/commands/prior-messages.js +42 -2
- package/dist/commands/prior-messages.js.map +1 -1
- package/dist/commands/schedule.d.ts.map +1 -1
- package/dist/commands/schedule.js +7 -5
- package/dist/commands/schedule.js.map +1 -1
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +60 -5
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/worktree.d.ts +6 -0
- package/dist/commands/worktree.d.ts.map +1 -0
- package/dist/commands/worktree.js +85 -0
- package/dist/commands/worktree.js.map +1 -0
- package/dist/config/composer-triggers.d.ts +41 -0
- package/dist/config/composer-triggers.d.ts.map +1 -0
- package/dist/config/composer-triggers.js +80 -0
- package/dist/config/composer-triggers.js.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +58 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +34 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/config/user-config.d.ts +2 -0
- package/dist/config/user-config.d.ts.map +1 -1
- package/dist/config/user-config.js +76 -6
- package/dist/config/user-config.js.map +1 -1
- package/dist/context/doctrine.d.ts +3 -3
- package/dist/context/doctrine.d.ts.map +1 -1
- package/dist/context/doctrine.js +3 -3
- package/dist/context/doctrine.js.map +1 -1
- package/dist/context/runtime-message.d.ts.map +1 -1
- package/dist/context/runtime-message.js +4 -0
- package/dist/context/runtime-message.js.map +1 -1
- package/dist/doctor/checks/credentials.d.ts.map +1 -1
- package/dist/doctor/checks/credentials.js +2 -0
- package/dist/doctor/checks/credentials.js.map +1 -1
- package/dist/integrations/mcp/oauth-store.d.ts +40 -0
- package/dist/integrations/mcp/oauth-store.d.ts.map +1 -0
- package/dist/integrations/mcp/oauth-store.js +549 -0
- package/dist/integrations/mcp/oauth-store.js.map +1 -0
- package/dist/integrations/mcp/servers.d.ts +72 -5
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +298 -24
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/mcp/user-config.d.ts +25 -0
- package/dist/integrations/mcp/user-config.d.ts.map +1 -0
- package/dist/integrations/mcp/user-config.js +92 -0
- package/dist/integrations/mcp/user-config.js.map +1 -0
- package/dist/integrations/plugins/runtime.d.ts +11 -9
- package/dist/integrations/plugins/runtime.d.ts.map +1 -1
- package/dist/integrations/plugins/runtime.js +17 -36
- package/dist/integrations/plugins/runtime.js.map +1 -1
- package/dist/integrations/providers/credential-store.d.ts +8 -0
- package/dist/integrations/providers/credential-store.d.ts.map +1 -1
- package/dist/integrations/providers/credential-store.js +111 -22
- package/dist/integrations/providers/credential-store.js.map +1 -1
- package/dist/integrations/providers/discover.d.ts +11 -5
- package/dist/integrations/providers/discover.d.ts.map +1 -1
- package/dist/integrations/providers/discover.js +26 -12
- package/dist/integrations/providers/discover.js.map +1 -1
- package/dist/integrations/providers/index.d.ts +1 -1
- package/dist/integrations/providers/index.d.ts.map +1 -1
- package/dist/integrations/providers/index.js +1 -1
- package/dist/integrations/providers/index.js.map +1 -1
- package/dist/integrations/resident/session-step.d.ts.map +1 -1
- package/dist/integrations/resident/session-step.js +2 -1
- package/dist/integrations/resident/session-step.js.map +1 -1
- package/dist/integrations/sessions/store.d.ts +23 -5
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +184 -26
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/subagents/NamzuCliAgent.d.ts +8 -0
- package/dist/integrations/subagents/NamzuCliAgent.d.ts.map +1 -0
- package/dist/integrations/subagents/NamzuCliAgent.js +9 -0
- package/dist/integrations/subagents/NamzuCliAgent.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +9 -6
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +108 -19
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/integrations/subagents/worktree-driver.d.ts +17 -0
- package/dist/integrations/subagents/worktree-driver.d.ts.map +1 -0
- package/dist/integrations/subagents/worktree-driver.js +72 -0
- package/dist/integrations/subagents/worktree-driver.js.map +1 -0
- package/dist/integrations/web/search.d.ts.map +1 -1
- package/dist/integrations/web/search.js +8 -6
- package/dist/integrations/web/search.js.map +1 -1
- package/dist/integrations/worktrees/managed.d.ts +49 -0
- package/dist/integrations/worktrees/managed.d.ts.map +1 -0
- package/dist/integrations/worktrees/managed.js +231 -0
- package/dist/integrations/worktrees/managed.js.map +1 -0
- package/dist/permissions/rules.d.ts +5 -1
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +85 -3
- package/dist/permissions/rules.js.map +1 -1
- package/dist/schedule/build.d.ts +19 -2
- package/dist/schedule/build.d.ts.map +1 -1
- package/dist/schedule/build.js +119 -21
- package/dist/schedule/build.js.map +1 -1
- package/dist/schedule/changes.d.ts.map +1 -1
- package/dist/schedule/changes.js +13 -0
- package/dist/schedule/changes.js.map +1 -1
- package/dist/schedule/commands/add.d.ts +1 -1
- package/dist/schedule/commands/add.d.ts.map +1 -1
- package/dist/schedule/commands/add.js +186 -40
- package/dist/schedule/commands/add.js.map +1 -1
- package/dist/schedule/commands/list.d.ts +2 -0
- package/dist/schedule/commands/list.d.ts.map +1 -1
- package/dist/schedule/commands/list.js +64 -19
- package/dist/schedule/commands/list.js.map +1 -1
- package/dist/schedule/daemon/daemon.d.ts +1 -1
- package/dist/schedule/daemon/daemon.d.ts.map +1 -1
- package/dist/schedule/daemon/daemon.js +17 -5
- package/dist/schedule/daemon/daemon.js.map +1 -1
- package/dist/schedule/daemon/notify.d.ts +21 -1
- package/dist/schedule/daemon/notify.d.ts.map +1 -1
- package/dist/schedule/daemon/notify.js +43 -0
- package/dist/schedule/daemon/notify.js.map +1 -1
- package/dist/schedule/fire/fire.d.ts +1 -1
- package/dist/schedule/fire/fire.d.ts.map +1 -1
- package/dist/schedule/fire/fire.js +205 -14
- package/dist/schedule/fire/fire.js.map +1 -1
- package/dist/schedule/fire/result.d.ts +1 -1
- package/dist/schedule/fire/result.d.ts.map +1 -1
- package/dist/schedule/fire/result.js +2 -1
- package/dist/schedule/fire/result.js.map +1 -1
- package/dist/schedule/fire/run-script.d.ts +46 -0
- package/dist/schedule/fire/run-script.d.ts.map +1 -0
- package/dist/schedule/fire/run-script.js +81 -0
- package/dist/schedule/fire/run-script.js.map +1 -0
- package/dist/schedule/fire/unattended-note.d.ts +8 -0
- package/dist/schedule/fire/unattended-note.d.ts.map +1 -1
- package/dist/schedule/fire/unattended-note.js +5 -0
- package/dist/schedule/fire/unattended-note.js.map +1 -1
- package/dist/schedule/fire/wake-gate.d.ts +39 -0
- package/dist/schedule/fire/wake-gate.d.ts.map +1 -0
- package/dist/schedule/fire/wake-gate.js +90 -0
- package/dist/schedule/fire/wake-gate.js.map +1 -0
- package/dist/schedule/floor.d.ts +1 -1
- package/dist/schedule/floor.d.ts.map +1 -1
- package/dist/schedule/floor.js +288 -16
- package/dist/schedule/floor.js.map +1 -1
- package/dist/schedule/policy.d.ts +40 -0
- package/dist/schedule/policy.d.ts.map +1 -1
- package/dist/schedule/policy.js +36 -0
- package/dist/schedule/policy.js.map +1 -1
- package/dist/schedule/script-check.d.ts +53 -0
- package/dist/schedule/script-check.d.ts.map +1 -0
- package/dist/schedule/script-check.js +145 -0
- package/dist/schedule/script-check.js.map +1 -0
- package/dist/schedule/script-shell.d.ts +3 -0
- package/dist/schedule/script-shell.d.ts.map +1 -0
- package/dist/schedule/script-shell.js +5 -0
- package/dist/schedule/script-shell.js.map +1 -0
- package/dist/schedule/service/manifest.js +1 -1
- package/dist/schedule/service/manifest.js.map +1 -1
- package/dist/schedule/store/atomic.d.ts +5 -2
- package/dist/schedule/store/atomic.d.ts.map +1 -1
- package/dist/schedule/store/atomic.js +7 -4
- package/dist/schedule/store/atomic.js.map +1 -1
- package/dist/schedule/store/claims.js +1 -1
- package/dist/schedule/store/claims.js.map +1 -1
- package/dist/schedule/store/history.d.ts +1 -1
- package/dist/schedule/store/history.d.ts.map +1 -1
- package/dist/schedule/store/history.js +4 -1
- package/dist/schedule/store/history.js.map +1 -1
- package/dist/schedule/store/jobs.d.ts +19 -3
- package/dist/schedule/store/jobs.d.ts.map +1 -1
- package/dist/schedule/store/jobs.js +83 -16
- package/dist/schedule/store/jobs.js.map +1 -1
- package/dist/schedule/store/state.js +1 -1
- package/dist/schedule/store/state.js.map +1 -1
- package/dist/schedule/types.d.ts +71 -6
- package/dist/schedule/types.d.ts.map +1 -1
- package/dist/schedule/types.js +25 -1
- package/dist/schedule/types.js.map +1 -1
- package/dist/schedule/visible-source.d.ts +18 -0
- package/dist/schedule/visible-source.d.ts.map +1 -0
- package/dist/schedule/visible-source.js +59 -0
- package/dist/schedule/visible-source.js.map +1 -0
- package/dist/skills/audit.d.ts +31 -0
- package/dist/skills/audit.d.ts.map +1 -0
- package/dist/skills/audit.js +75 -0
- package/dist/skills/audit.js.map +1 -0
- package/dist/test-support/toolset.d.ts +23 -0
- package/dist/test-support/toolset.d.ts.map +1 -0
- package/dist/test-support/toolset.js +27 -0
- package/dist/test-support/toolset.js.map +1 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +568 -106
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/ChoicePicker.d.ts +2 -0
- package/dist/tui/ChoicePicker.d.ts.map +1 -1
- package/dist/tui/ChoicePicker.js +10 -2
- package/dist/tui/ChoicePicker.js.map +1 -1
- package/dist/tui/Composer.d.ts +62 -3
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +162 -25
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/ComposerFrame.d.ts +1 -1
- package/dist/tui/ComposerFrame.js +3 -3
- package/dist/tui/ComposerFrame.js.map +1 -1
- package/dist/tui/EffortSlider.d.ts +8 -9
- package/dist/tui/EffortSlider.d.ts.map +1 -1
- package/dist/tui/EffortSlider.js +15 -12
- package/dist/tui/EffortSlider.js.map +1 -1
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +18 -3
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/ResumePicker.d.ts +2 -1
- package/dist/tui/ResumePicker.d.ts.map +1 -1
- package/dist/tui/ResumePicker.js +3 -3
- package/dist/tui/ResumePicker.js.map +1 -1
- package/dist/tui/StatusBar.d.ts +12 -12
- package/dist/tui/StatusBar.d.ts.map +1 -1
- package/dist/tui/StatusBar.js +15 -15
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/agent.d.ts +46 -7
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +399 -182
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/browser-notices.d.ts.map +1 -1
- package/dist/tui/browser-notices.js +6 -1
- package/dist/tui/browser-notices.js.map +1 -1
- package/dist/tui/credential-entry.d.ts +4 -29
- package/dist/tui/credential-entry.d.ts.map +1 -1
- package/dist/tui/credential-entry.js +7 -29
- package/dist/tui/credential-entry.js.map +1 -1
- package/dist/tui/hypermode.d.ts +37 -0
- package/dist/tui/hypermode.d.ts.map +1 -0
- package/dist/tui/hypermode.js +58 -0
- package/dist/tui/hypermode.js.map +1 -0
- package/dist/tui/login-prompt.d.ts +3 -1
- package/dist/tui/login-prompt.d.ts.map +1 -1
- package/dist/tui/login-prompt.js +11 -0
- package/dist/tui/login-prompt.js.map +1 -1
- package/dist/tui/permission-review.d.ts +1 -1
- package/dist/tui/permission-review.js +1 -1
- package/dist/tui/provider-list.d.ts.map +1 -1
- package/dist/tui/provider-list.js +3 -1
- package/dist/tui/provider-list.js.map +1 -1
- package/dist/tui/schedule/host-commands.d.ts.map +1 -1
- package/dist/tui/schedule/host-commands.js +94 -33
- package/dist/tui/schedule/host-commands.js.map +1 -1
- package/dist/tui/schedule/integration.d.ts +6 -2
- package/dist/tui/schedule/integration.d.ts.map +1 -1
- package/dist/tui/schedule/integration.js +3 -1
- package/dist/tui/schedule/integration.js.map +1 -1
- package/dist/tui/schedule/resume.d.ts.map +1 -1
- package/dist/tui/schedule/resume.js +10 -1
- package/dist/tui/schedule/resume.js.map +1 -1
- package/dist/tui/schedule/tool-host.d.ts +4 -1
- package/dist/tui/schedule/tool-host.d.ts.map +1 -1
- package/dist/tui/schedule/tool-host.js +155 -40
- package/dist/tui/schedule/tool-host.js.map +1 -1
- package/dist/tui/slashCommands.d.ts +33 -6
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +96 -34
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/theme.d.ts +10 -4
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +4 -3
- package/dist/tui/theme.js.map +1 -1
- package/dist/tui/triggers/analyze.d.ts +57 -0
- package/dist/tui/triggers/analyze.d.ts.map +1 -0
- package/dist/tui/triggers/analyze.js +221 -0
- package/dist/tui/triggers/analyze.js.map +1 -0
- package/dist/tui/triggers/context-text.d.ts +21 -0
- package/dist/tui/triggers/context-text.d.ts.map +1 -0
- package/dist/tui/triggers/context-text.js +44 -0
- package/dist/tui/triggers/context-text.js.map +1 -0
- package/dist/tui/triggers/copy.d.ts +38 -0
- package/dist/tui/triggers/copy.d.ts.map +1 -0
- package/dist/tui/triggers/copy.js +163 -0
- package/dist/tui/triggers/copy.js.map +1 -0
- package/dist/tui/triggers/detect.d.ts +83 -0
- package/dist/tui/triggers/detect.d.ts.map +1 -0
- package/dist/tui/triggers/detect.js +295 -0
- package/dist/tui/triggers/detect.js.map +1 -0
- package/dist/tui/triggers/fold.d.ts +34 -0
- package/dist/tui/triggers/fold.d.ts.map +1 -0
- package/dist/tui/triggers/fold.js +44 -0
- package/dist/tui/triggers/fold.js.map +1 -0
- package/dist/tui/triggers/pattern.d.ts +66 -0
- package/dist/tui/triggers/pattern.d.ts.map +1 -0
- package/dist/tui/triggers/pattern.js +243 -0
- package/dist/tui/triggers/pattern.js.map +1 -0
- package/dist/tui/triggers/provenance.d.ts +58 -0
- package/dist/tui/triggers/provenance.d.ts.map +1 -0
- package/dist/tui/triggers/provenance.js +98 -0
- package/dist/tui/triggers/provenance.js.map +1 -0
- package/dist/tui/triggers/registry.d.ts +113 -0
- package/dist/tui/triggers/registry.d.ts.map +1 -0
- package/dist/tui/triggers/registry.js +164 -0
- package/dist/tui/triggers/registry.js.map +1 -0
- package/dist/tui/triggers/setting.d.ts +18 -0
- package/dist/tui/triggers/setting.d.ts.map +1 -0
- package/dist/tui/triggers/setting.js +61 -0
- package/dist/tui/triggers/setting.js.map +1 -0
- package/dist/tui/triggers/verbs.d.ts +29 -0
- package/dist/tui/triggers/verbs.d.ts.map +1 -0
- package/dist/tui/triggers/verbs.js +140 -0
- package/dist/tui/triggers/verbs.js.map +1 -0
- package/dist/tui/turn-interruption.d.ts.map +1 -1
- package/dist/tui/turn-interruption.js +4 -0
- package/dist/tui/turn-interruption.js.map +1 -1
- package/dist/tui/types.d.ts +3 -1
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +13 -12
- package/skills/schedule-task/SKILL.md +115 -3
package/dist/tui/App.js
CHANGED
|
@@ -19,15 +19,16 @@ import { ToolOutputViewer } from './ToolOutputViewer.js';
|
|
|
19
19
|
import { join, relative } from 'node:path';
|
|
20
20
|
import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, TurnCancelled, asSessionId, createAssistantMessage, createUserMessage, generateSessionId, generateTurnId, isCompactionMessage, kernelHostCommands, SKILL_TOOL_NAME, } from '@namzu/sdk';
|
|
21
21
|
import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
|
|
22
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
22
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
23
23
|
import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
|
|
24
24
|
import { visibleProjectInstructionPath } from '../context/project-path.js';
|
|
25
25
|
import { runtimeContextLabel } from '../context/runtime-message.js';
|
|
26
26
|
import { writeClipboardText } from '../integrations/clipboard/text.js';
|
|
27
27
|
import { terminalNotificationEnabled, writeTerminalNotification, } from '../integrations/notifications/terminal.js';
|
|
28
|
-
import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, clearStoredCodexCredential, clearStoredSubscriptionCredential, credentialsPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
|
|
29
|
-
import { activeConversationTurn, archiveConversation, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, loadResumableConversation, openSessions, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
|
|
28
|
+
import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, clearStoredCodexCredential, clearStoredGeminiApiKey as clearStoredGoogleApiKey, clearStoredSubscriptionCredential, credentialsPath, googleApiKeyPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredGeminiApiKey as readStoredGoogleApiKey, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, writeStoredGeminiApiKey as writeStoredGoogleApiKey, } from '../integrations/providers/index.js';
|
|
29
|
+
import { ConversationIndexRefreshError, activeConversationTurn, archiveConversation, forkConversation, forkConversationBeforeUser, listArchived, listRecent, loadConversation, loadResumableConversation, openSessions, requireWritableConversation, setTitle, startConversation, titleOf, unarchiveConversation, } from '../integrations/sessions/store.js';
|
|
30
30
|
import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
|
|
31
|
+
import { openManagedWorktrees, worktreeOpenHint } from '../integrations/worktrees/managed.js';
|
|
31
32
|
import { NARRATION_TOOL_NAME } from '../integrations/subagents/runtime.js';
|
|
32
33
|
import { agentsSlashCommand } from '../integrations/subagents/slash.js';
|
|
33
34
|
import { isTrusted, trustDir } from '../integrations/trust/store.js';
|
|
@@ -43,8 +44,15 @@ import { BrandHeader } from './BrandHeader.js';
|
|
|
43
44
|
import { ChoicePicker } from './ChoicePicker.js';
|
|
44
45
|
import { emptyPluginReport, pluginDetails, pluginOption, pluginStartupState } from './plugin-view.js';
|
|
45
46
|
import { Composer, suggestionWindowSize, } from './Composer.js';
|
|
47
|
+
import { resolveComposerTriggers } from '../config/composer-triggers.js';
|
|
48
|
+
import { triggerContextTexts, triggerEffort } from './triggers/context-text.js';
|
|
49
|
+
import { transcriptTagLine } from './triggers/copy.js';
|
|
50
|
+
import { unavailable as triggerUnavailable } from './triggers/detect.js';
|
|
51
|
+
import { BUILTIN_TRIGGERS, compileRegistry, triggerDefinition, } from './triggers/registry.js';
|
|
52
|
+
import { describeComposerTriggers, setComposerTriggers } from './triggers/setting.js';
|
|
46
53
|
import { ComposerFrame } from './ComposerFrame.js';
|
|
47
54
|
import { EffortSlider, effortSliderLayout } from './EffortSlider.js';
|
|
55
|
+
import { HYPERMODE, HYPERMODE_SUMMARY, ORCHESTRATE_ALIAS_NOTICE, hypermodeEffort, hypermodeStopLabel, } from './hypermode.js';
|
|
48
56
|
import { CopyPicker } from './CopyPicker.js';
|
|
49
57
|
import { EditPromptPicker } from './EditPromptPicker.js';
|
|
50
58
|
import { LiveActivity, formatElapsed } from './LiveActivity.js';
|
|
@@ -81,7 +89,7 @@ import { checklistInView, liveWindow, settledBeforeStreaming } from './live-wind
|
|
|
81
89
|
import { ViewportBound } from './ViewportBound.js';
|
|
82
90
|
import { resolveModelSwitch, } from './model-switch.js';
|
|
83
91
|
import { parseModelSelectionIntent, resolveModelSelectionIntent } from './model-selection-intent.js';
|
|
84
|
-
import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart,
|
|
92
|
+
import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeAllCredentialsLogout, describeProviderLogout, } from './login-prompt.js';
|
|
85
93
|
import { selectPrimaryProvider } from './provider-selection.js';
|
|
86
94
|
import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
|
|
87
95
|
import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
|
|
@@ -94,6 +102,7 @@ import { moveSelection } from './selection-window.js';
|
|
|
94
102
|
import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
|
|
95
103
|
import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderAgents, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
|
|
96
104
|
import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
|
|
105
|
+
import { terminalDisplayText } from './terminal-display.js';
|
|
97
106
|
import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
|
|
98
107
|
import { theme } from './theme.js';
|
|
99
108
|
import { useSelectionIndex } from './use-selection-index.js';
|
|
@@ -415,11 +424,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
415
424
|
// A session setting layered above effort, not a level a provider publishes
|
|
416
425
|
// — see docs/cli/slash-commands.md's /effort section. In-memory and
|
|
417
426
|
// per-session like reasoningEffort above; no preferences file involved.
|
|
418
|
-
const [
|
|
419
|
-
const
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
427
|
+
const [hypermode, setHypermodeState] = useState(false);
|
|
428
|
+
const hypermodeRef = useRef(false);
|
|
429
|
+
const setHypermode = useCallback((next) => {
|
|
430
|
+
hypermodeRef.current = next;
|
|
431
|
+
setHypermodeState(next);
|
|
423
432
|
}, []);
|
|
424
433
|
const [activeSkills, setActiveSkills] = useState([]);
|
|
425
434
|
// Read when the session comes up rather than on every keystroke: the
|
|
@@ -983,7 +992,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
983
992
|
ask: (question, signal) => scheduleLiveRef.current?.ask(question, signal) ?? Promise.resolve({ kind: 'skip' }),
|
|
984
993
|
askPermission: (request) => scheduleLiveRef.current?.askPermission(request) ??
|
|
985
994
|
Promise.resolve({ kind: 'reject', feedback: 'Nobody can answer yet.' }),
|
|
986
|
-
submit: (text) => scheduleLiveRef.current?.submit(text),
|
|
995
|
+
submit: (text, createdBy) => scheduleLiveRef.current?.submit(text, createdBy),
|
|
987
996
|
});
|
|
988
997
|
}
|
|
989
998
|
/**
|
|
@@ -1158,9 +1167,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1158
1167
|
// outlived the row it named would resolve `/expand 3` to output the operator
|
|
1159
1168
|
// can no longer see anywhere, which is the failure this surface is being
|
|
1160
1169
|
// cleaned of, one layer up.
|
|
1161
|
-
const resetTranscript = useCallback(() => {
|
|
1170
|
+
const resetTranscript = useCallback((options = {}) => {
|
|
1162
1171
|
if (stdout.isTTY)
|
|
1163
1172
|
writeStdout('\x1b[2J\x1b[3J\x1b[H');
|
|
1173
|
+
if (!options.preserveOutputViewer)
|
|
1174
|
+
setOutputViewer(null);
|
|
1164
1175
|
// The scrollback floor goes with the log it counted. <Static> is remounted
|
|
1165
1176
|
// by the key below and has emitted nothing again; a floor that survived
|
|
1166
1177
|
// would keep the next conversation's rows out of the live window.
|
|
@@ -1172,12 +1183,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1172
1183
|
const previous = previousTerminalRef.current;
|
|
1173
1184
|
previousTerminalRef.current = terminal;
|
|
1174
1185
|
if (externalEditorRequestRef.current === null &&
|
|
1175
|
-
(terminal.columns
|
|
1176
|
-
// A contraction can
|
|
1177
|
-
// receives resize
|
|
1178
|
-
//
|
|
1179
|
-
// messages, draft and selected child. Regular frames
|
|
1180
|
-
|
|
1186
|
+
(terminal.columns !== previous.columns || terminal.rows < previous.rows)) {
|
|
1187
|
+
// A contraction can move live rows into native scrollback before Ink
|
|
1188
|
+
// receives resize; expansion leaves already-printed Static rows wrapped
|
|
1189
|
+
// at their old width. Rebuild the normal-buffer transcript at the new
|
|
1190
|
+
// width, retaining messages, draft and selected child. Regular frames
|
|
1191
|
+
// remain incremental.
|
|
1192
|
+
resetTranscript({ preserveOutputViewer: true });
|
|
1181
1193
|
}
|
|
1182
1194
|
}, [terminal, resetTranscript]);
|
|
1183
1195
|
const requestExternalEditor = useCallback((seed) => {
|
|
@@ -1297,6 +1309,46 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1297
1309
|
const learningRef = useRef({ generation: -1, proposed: false, skillUsed: false });
|
|
1298
1310
|
/** `/skills save off|on` in this session, over whatever the config files say. */
|
|
1299
1311
|
const skillSuggestOverrideRef = useRef(undefined);
|
|
1312
|
+
/** `/config triggers on|off` in this session, over whatever the config files say. */
|
|
1313
|
+
const [composerTriggersOverride, setComposerTriggersOverride] = useState();
|
|
1314
|
+
const composerTriggerSettings = useMemo(() => resolveComposerTriggers(ctx.composerTriggers), [ctx.composerTriggers]);
|
|
1315
|
+
const composerTriggerRegistry = useMemo(() => compileRegistry(BUILTIN_TRIGGERS, {
|
|
1316
|
+
arming: composerTriggerSettings.arming,
|
|
1317
|
+
locales: composerTriggerSettings.languages,
|
|
1318
|
+
}), [composerTriggerSettings]);
|
|
1319
|
+
const composerTriggersOn = composerTriggersOverride ?? composerTriggerSettings.enabled;
|
|
1320
|
+
/**
|
|
1321
|
+
* Whether the model can run `skill-creator` here, which save-as-skill
|
|
1322
|
+
* needs. Read once for the tag row; read again when a turn starts and when
|
|
1323
|
+
* its follow-up would run, in case the skills changed meanwhile.
|
|
1324
|
+
*/
|
|
1325
|
+
const readSkillCreator = useCallback(() => {
|
|
1326
|
+
try {
|
|
1327
|
+
const creator = discoverSkills({
|
|
1328
|
+
cwd: ctx.cwd,
|
|
1329
|
+
...(ctx.skills ? { config: ctx.skills } : {}),
|
|
1330
|
+
}).find((skill) => skill.name === SKILL_CREATOR_SKILL);
|
|
1331
|
+
return Boolean(creator && !creator.problem && creator.invocation !== 'operator');
|
|
1332
|
+
}
|
|
1333
|
+
catch {
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
}, [ctx.cwd, ctx.skills]);
|
|
1337
|
+
const skillCreatorAvailable = useMemo(() => readSkillCreator(), [readSkillCreator]);
|
|
1338
|
+
/** The session as the triggers see it right now, for a check at a turn's start or end. */
|
|
1339
|
+
const triggerContextNow = useCallback(() => ({
|
|
1340
|
+
permissionMode: permissionModeRef.current,
|
|
1341
|
+
sessionHypermode: hypermodeRef.current,
|
|
1342
|
+
effortMenu: (session?.reasoningEffortLevels?.length ?? 0) > 0,
|
|
1343
|
+
// The interactive session always mounts `Agent` and the `schedule`
|
|
1344
|
+
// tool; only scheduled runs withhold them, and those never read
|
|
1345
|
+
// composer triggers.
|
|
1346
|
+
agentTool: true,
|
|
1347
|
+
skillCreator: readSkillCreator(),
|
|
1348
|
+
scheduleTool: true,
|
|
1349
|
+
}), [readSkillCreator, session]);
|
|
1350
|
+
/** A turn's hand-off to its after-turn triggers; assigned below, where the queue lives. */
|
|
1351
|
+
const triggerFollowUpsRef = useRef(null);
|
|
1300
1352
|
const conversationLearning = useCallback(() => {
|
|
1301
1353
|
if (learningRef.current.generation !== conversationGenRef.current) {
|
|
1302
1354
|
learningRef.current = {
|
|
@@ -1613,22 +1665,28 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1613
1665
|
pushMessage('system', `Reasoning: ${effort} (this session).`);
|
|
1614
1666
|
}, [hasUnsettledTurn, pushMessage, session, setReasoningEffort, state]);
|
|
1615
1667
|
/**
|
|
1616
|
-
* Turn the session's
|
|
1668
|
+
* Turn the session's hypermode on or off.
|
|
1617
1669
|
*
|
|
1618
1670
|
* On: pins effort to the model's highest published level (last entry of
|
|
1619
1671
|
* `reasoningEffortLevels`, which every provider publishes low-to-high) and
|
|
1620
|
-
* strengthens delegation guidance for future turns via `SendOptions.
|
|
1672
|
+
* strengthens delegation guidance for future turns via `SendOptions.hypermode`
|
|
1621
1673
|
* — see `applyReasoningEffort` above for the mirrored guard shape. When the
|
|
1622
1674
|
* model publishes no exact menu, effort is left alone and the notice says
|
|
1623
1675
|
* so explicitly rather than silently doing nothing.
|
|
1624
1676
|
*/
|
|
1625
|
-
|
|
1677
|
+
/**
|
|
1678
|
+
* The effort in force before hypermode pinned the highest level, and the
|
|
1679
|
+
* level it pinned. Turning the mode off puts the effort back — unless the
|
|
1680
|
+
* operator chose another level meanwhile, which then stays.
|
|
1681
|
+
*/
|
|
1682
|
+
const hypermodePinRef = useRef(null);
|
|
1683
|
+
const applyHypermode = useCallback((enabled, selectedSession) => {
|
|
1626
1684
|
if (!session?.hasProvider) {
|
|
1627
|
-
pushMessage('system', 'No active session — pick a provider before turning
|
|
1685
|
+
pushMessage('system', 'No active session — pick a provider before turning hypermode on.');
|
|
1628
1686
|
return;
|
|
1629
1687
|
}
|
|
1630
1688
|
if (selectedSession !== undefined && selectedSession !== session) {
|
|
1631
|
-
pushMessage('system', '
|
|
1689
|
+
pushMessage('system', 'Hypermode was not changed: the selected model is no longer active.');
|
|
1632
1690
|
return;
|
|
1633
1691
|
}
|
|
1634
1692
|
if (state !== 'idle' ||
|
|
@@ -1637,22 +1695,38 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1637
1695
|
(queuedRef.current.length > 0 && selectedSession !== session) ||
|
|
1638
1696
|
permissionResolveRef.current !== null ||
|
|
1639
1697
|
compactingRef.current) {
|
|
1640
|
-
pushMessage('system', '
|
|
1698
|
+
pushMessage('system', 'Hypermode was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
|
|
1641
1699
|
return;
|
|
1642
1700
|
}
|
|
1643
|
-
|
|
1701
|
+
if (!enabled && !hypermodeRef.current) {
|
|
1702
|
+
pushMessage('system', 'Hypermode is off.');
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
setHypermode(enabled);
|
|
1644
1706
|
if (!enabled) {
|
|
1645
|
-
|
|
1707
|
+
const pin = hypermodePinRef.current;
|
|
1708
|
+
hypermodePinRef.current = null;
|
|
1709
|
+
if (pin && reasoningEffortRef.current === pin.pinned) {
|
|
1710
|
+
setReasoningEffort(pin.before);
|
|
1711
|
+
pushMessage('system', `Hypermode is off — effort back to ${pin.before ?? 'the provider default'}.`);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
pushMessage('system', 'Hypermode is off.');
|
|
1646
1715
|
return;
|
|
1647
1716
|
}
|
|
1648
|
-
const
|
|
1649
|
-
if (
|
|
1650
|
-
|
|
1651
|
-
|
|
1717
|
+
const pinned = hypermodeEffort(session.reasoningEffortLevels);
|
|
1718
|
+
if (pinned !== undefined) {
|
|
1719
|
+
// Turning it on twice keeps the effort from before the first time.
|
|
1720
|
+
if (!hypermodePinRef.current)
|
|
1721
|
+
hypermodePinRef.current = { before: reasoningEffortRef.current, pinned };
|
|
1722
|
+
else
|
|
1723
|
+
hypermodePinRef.current = { ...hypermodePinRef.current, pinned };
|
|
1724
|
+
setReasoningEffort(pinned);
|
|
1725
|
+
pushMessage('system', `Hypermode is on — effort pinned to ${pinned} for ${session.modelSummary ?? 'this model'}, and delegation guidance is strengthened for this session.`);
|
|
1652
1726
|
return;
|
|
1653
1727
|
}
|
|
1654
|
-
pushMessage('system', `
|
|
1655
|
-
}, [hasUnsettledTurn, pushMessage, session,
|
|
1728
|
+
pushMessage('system', `Hypermode is on — ${session.modelSummary ?? 'this model'} does not publish an exact effort menu, so effort was left as is. Delegation guidance is still strengthened for this session.`);
|
|
1729
|
+
}, [hasUnsettledTurn, pushMessage, session, setHypermode, setReasoningEffort, state]);
|
|
1656
1730
|
const stepReasoningEffort = useCallback((direction) => {
|
|
1657
1731
|
const levels = session?.reasoningEffortLevels;
|
|
1658
1732
|
if (!session?.hasProvider || levels === undefined || levels.length === 0) {
|
|
@@ -1733,26 +1807,44 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1733
1807
|
}
|
|
1734
1808
|
}, [conversationLearning, ctx.cwd, ctx.skills, pushMessage]);
|
|
1735
1809
|
const removeStoredCredential = useCallback((target) => {
|
|
1736
|
-
const path = credentialsPath();
|
|
1810
|
+
const path = target === 'google' ? googleApiKeyPath() : credentialsPath();
|
|
1737
1811
|
const hadClaude = readStoredSubscriptionCredential() !== null;
|
|
1738
1812
|
const hadCodex = readStoredCodexCredential() !== null;
|
|
1813
|
+
const hadGoogleKey = readStoredGoogleApiKey() !== null;
|
|
1814
|
+
if (target === 'all') {
|
|
1815
|
+
const failures = [];
|
|
1816
|
+
try {
|
|
1817
|
+
clearAllStoredCredentials();
|
|
1818
|
+
}
|
|
1819
|
+
catch (error) {
|
|
1820
|
+
failures.push(`${credentialsPath()}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1821
|
+
}
|
|
1822
|
+
try {
|
|
1823
|
+
clearStoredGoogleApiKey();
|
|
1824
|
+
}
|
|
1825
|
+
catch (error) {
|
|
1826
|
+
failures.push(`${googleApiKeyPath()}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1827
|
+
}
|
|
1828
|
+
if (failures.length > 0) {
|
|
1829
|
+
pushMessage('system', `Some stored credentials could not be removed: ${failures.join('; ')}. Check both stores before retrying.`);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
pushMessage('system', describeAllCredentialsLogout(path, googleApiKeyPath(), hadClaude || hadCodex || hadGoogleKey));
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1739
1835
|
try {
|
|
1740
1836
|
if (target === 'anthropic')
|
|
1741
1837
|
clearStoredSubscriptionCredential();
|
|
1742
1838
|
else if (target === 'codex')
|
|
1743
1839
|
clearStoredCodexCredential();
|
|
1744
1840
|
else
|
|
1745
|
-
|
|
1841
|
+
clearStoredGoogleApiKey();
|
|
1746
1842
|
}
|
|
1747
1843
|
catch (error) {
|
|
1748
1844
|
pushMessage('system', `Could not remove ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1749
1845
|
return;
|
|
1750
1846
|
}
|
|
1751
|
-
|
|
1752
|
-
pushMessage('system', describeLogout(path, hadClaude || hadCodex));
|
|
1753
|
-
return;
|
|
1754
|
-
}
|
|
1755
|
-
pushMessage('system', describeProviderLogout(path, target, target === 'anthropic' ? hadClaude : hadCodex));
|
|
1847
|
+
pushMessage('system', describeProviderLogout(path, target, target === 'anthropic' ? hadClaude : target === 'codex' ? hadCodex : hadGoogleKey));
|
|
1756
1848
|
}, [pushMessage]);
|
|
1757
1849
|
/** Publish one recoverable, read-only tombstone before leaving the TUI. */
|
|
1758
1850
|
const archiveCurrentConversation = useCallback(() => {
|
|
@@ -1795,7 +1887,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
1795
1887
|
closeAndExit();
|
|
1796
1888
|
}
|
|
1797
1889
|
catch (error) {
|
|
1798
|
-
pushMessage('system',
|
|
1890
|
+
pushMessage('system', error instanceof ConversationIndexRefreshError
|
|
1891
|
+
? error.message
|
|
1892
|
+
: `Could not archive this conversation: ${error instanceof Error ? error.message : String(error)}`);
|
|
1799
1893
|
}
|
|
1800
1894
|
finally {
|
|
1801
1895
|
if (!archived && conversationMutationRef.current === 'archive') {
|
|
@@ -2214,8 +2308,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2214
2308
|
removeStoredCredential(value);
|
|
2215
2309
|
return;
|
|
2216
2310
|
}
|
|
2217
|
-
if (picker.kind === 'reasoning-effort' && value ===
|
|
2218
|
-
|
|
2311
|
+
if (picker.kind === 'reasoning-effort' && value === HYPERMODE_VALUE) {
|
|
2312
|
+
applyHypermode(!hypermodeRef.current, picker.selectedSession);
|
|
2219
2313
|
return;
|
|
2220
2314
|
}
|
|
2221
2315
|
applyReasoningEffort(value, picker.selectedSession);
|
|
@@ -2223,7 +2317,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2223
2317
|
activateSkill,
|
|
2224
2318
|
advanceQueueContinuation,
|
|
2225
2319
|
appLifetime.signal,
|
|
2226
|
-
|
|
2320
|
+
applyHypermode,
|
|
2227
2321
|
applyPermissionMode,
|
|
2228
2322
|
applyReasoningEffort,
|
|
2229
2323
|
archiveCurrentConversation,
|
|
@@ -2546,11 +2640,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2546
2640
|
// A picker-owned provider/model change is one state transition. Clear the
|
|
2547
2641
|
// old model's effort selection before publishing the replacement session
|
|
2548
2642
|
// or releasing any paused queue. Failed and superseded candidates returned
|
|
2549
|
-
// above, so they leave the current session selection untouched.
|
|
2643
|
+
// above, so they leave the current session selection untouched. Hypermode
|
|
2550
2644
|
// mode survives the switch: re-pin to the new model's highest published
|
|
2551
2645
|
// level instead of clearing, exactly as it pinned when first turned on.
|
|
2552
2646
|
if (signal !== undefined) {
|
|
2553
|
-
|
|
2647
|
+
const repinned = hypermodeRef.current ? hypermodeEffort(s.reasoningEffortLevels) : undefined;
|
|
2648
|
+
// A new model starts at its own default; that is what turning
|
|
2649
|
+
// hypermode off returns to now.
|
|
2650
|
+
hypermodePinRef.current = repinned ? { before: undefined, pinned: repinned } : null;
|
|
2651
|
+
setReasoningEffort(repinned);
|
|
2554
2652
|
}
|
|
2555
2653
|
// Re-hydration (a provider switch via /model) builds a second session;
|
|
2556
2654
|
// without this the first one's tool-server child processes stay alive
|
|
@@ -2559,9 +2657,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2559
2657
|
void previousSessionRef.current?.close();
|
|
2560
2658
|
previousSessionRef.current = s;
|
|
2561
2659
|
setSession(s);
|
|
2562
|
-
//
|
|
2660
|
+
// Hypermode already re-pinned effort above; reopening this picker
|
|
2563
2661
|
// would ask the operator to redo a choice the mode just made for them.
|
|
2564
|
-
if (!
|
|
2662
|
+
if (!hypermodeRef.current && options.chooseReasoningEffort && s.reasoningEffortLevels?.length) {
|
|
2565
2663
|
// Own input before publishing ready or releasing a paused queue. The
|
|
2566
2664
|
// menu closes only after choosing an effort or keeping the new default.
|
|
2567
2665
|
setSelectedChoice(0);
|
|
@@ -2575,10 +2673,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2575
2673
|
setMentionCandidates(files);
|
|
2576
2674
|
});
|
|
2577
2675
|
setCurrentProvider(primaryProvider(prefs).id);
|
|
2578
|
-
//
|
|
2579
|
-
//
|
|
2580
|
-
//
|
|
2581
|
-
|
|
2676
|
+
// Failed and superseded provider choices returned above. A usable
|
|
2677
|
+
// provider change can recover a failed turn's dependent queue, but a
|
|
2678
|
+
// paused turn still owns the conversation's durable active slot. Keep
|
|
2679
|
+
// that queue held until its checkpoint is resumed and settles.
|
|
2680
|
+
if (signal !== undefined && queuePauseRef.current?.outcome !== 'paused')
|
|
2582
2681
|
advanceQueueContinuation();
|
|
2583
2682
|
if (s.hasProvider) {
|
|
2584
2683
|
setTranscriptOwned(true);
|
|
@@ -2971,9 +3070,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
2971
3070
|
// terminal scrollback; the live tail stays deliberately small so an activity
|
|
2972
3071
|
// tick never repaints the whole conversation.
|
|
2973
3072
|
//
|
|
2974
|
-
useEffect(() => {
|
|
2975
|
-
setOutputViewer(null);
|
|
2976
|
-
}, [resetKey]);
|
|
2977
3073
|
useEffect(() => {
|
|
2978
3074
|
if (permission !== null)
|
|
2979
3075
|
setOutputViewer(null);
|
|
@@ -3112,7 +3208,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3112
3208
|
const scope = scopeRef.current;
|
|
3113
3209
|
if (!sessions || !scope || !conversationMaterializedRef.current || !session?.hasProvider)
|
|
3114
3210
|
return false;
|
|
3115
|
-
const
|
|
3211
|
+
const resumedSessionId = scope.sessionId;
|
|
3212
|
+
const active = await activeConversationTurn(sessions, resumedSessionId).catch(() => undefined);
|
|
3116
3213
|
if (!active)
|
|
3117
3214
|
return false;
|
|
3118
3215
|
if (abortRef.current || state !== 'idle') {
|
|
@@ -3193,7 +3290,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3193
3290
|
finalizeMessage(st.assistantId);
|
|
3194
3291
|
// The resumed segment was appended to the log; the fold is the context
|
|
3195
3292
|
// the next prompt continues from.
|
|
3196
|
-
modelHistoryRef.current = await loadConversation(sessions,
|
|
3293
|
+
modelHistoryRef.current = await loadConversation(sessions, resumedSessionId);
|
|
3294
|
+
// A successfully completed checkpoint frees the conversation's active
|
|
3295
|
+
// slot. Only then may prompts queued behind the paused turn start. A
|
|
3296
|
+
// repeated pause, failed resume, unreadable log, or conversation switch
|
|
3297
|
+
// keeps dependent work held for an explicit operator decision.
|
|
3298
|
+
if (st.completed &&
|
|
3299
|
+
queuePauseRef.current?.outcome === 'paused' &&
|
|
3300
|
+
scopeRef.current?.sessionId === resumedSessionId &&
|
|
3301
|
+
(await activeConversationTurn(sessions, resumedSessionId)
|
|
3302
|
+
.then((next) => next === undefined)
|
|
3303
|
+
.catch(() => false))) {
|
|
3304
|
+
advanceQueueContinuation();
|
|
3305
|
+
}
|
|
3197
3306
|
}
|
|
3198
3307
|
catch (err) {
|
|
3199
3308
|
pushMessage('system', `Could not resume the turn: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -3210,7 +3319,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3210
3319
|
await restoreBrowser?.().catch(() => undefined);
|
|
3211
3320
|
}
|
|
3212
3321
|
return true;
|
|
3213
|
-
}, [detected, finalizeMessage, flushStream, pushMessage, session, setHandoffPark, state]);
|
|
3322
|
+
}, [advanceQueueContinuation, detected, finalizeMessage, flushStream, pushMessage, session, setHandoffPark, state]);
|
|
3214
3323
|
// `namzu resume <id>` of a scheduled run parked on a decision: what the
|
|
3215
3324
|
// notification and `/schedule` tell the operator to run. Continue it once
|
|
3216
3325
|
// the screen is ready, exactly as `/resume` would, so the parked call is on
|
|
@@ -3258,6 +3367,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3258
3367
|
pushMessage('system', `Could not list conversations: ${err instanceof Error ? err.message : String(err)}`);
|
|
3259
3368
|
}
|
|
3260
3369
|
}, [ensureSessions, pushMessage, resumeActiveTurn]);
|
|
3370
|
+
const doUnarchive = useCallback(async () => {
|
|
3371
|
+
if (state !== 'idle' ||
|
|
3372
|
+
abortRef.current !== null ||
|
|
3373
|
+
hasUnsettledTurn() ||
|
|
3374
|
+
queuedRef.current.length > 0 ||
|
|
3375
|
+
compactingRef.current ||
|
|
3376
|
+
exportingRef.current) {
|
|
3377
|
+
pushMessage('system', 'Finish the current turn and queued work before restoring a conversation.');
|
|
3378
|
+
return;
|
|
3379
|
+
}
|
|
3380
|
+
const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
|
|
3381
|
+
if (!sessions) {
|
|
3382
|
+
pushMessage('system', 'Conversation history is unavailable in this folder.');
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
try {
|
|
3386
|
+
const archived = await listArchived(sessions, 100);
|
|
3387
|
+
if (archived.length === 0) {
|
|
3388
|
+
pushMessage('system', 'No archived conversations in this project.');
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
setResumeList(archived);
|
|
3392
|
+
setSelectedResume(0);
|
|
3393
|
+
setPhase('unarchive');
|
|
3394
|
+
}
|
|
3395
|
+
catch (err) {
|
|
3396
|
+
pushMessage('system', `Could not list archived conversations: ${err instanceof Error ? err.message : String(err)}`);
|
|
3397
|
+
}
|
|
3398
|
+
}, [ensureSessions, hasUnsettledTurn, pushMessage, state]);
|
|
3261
3399
|
// Resolve a pending permission prompt with the user's decision and tear
|
|
3262
3400
|
// down the overlay. No-op if nothing is pending.
|
|
3263
3401
|
const resolvePermission = useCallback((decision) => {
|
|
@@ -3438,6 +3576,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3438
3576
|
resumeCommittedRef.current = true;
|
|
3439
3577
|
let msgs;
|
|
3440
3578
|
try {
|
|
3579
|
+
await requireWritableConversation(sessions, conv.id, 'resume conversation');
|
|
3441
3580
|
msgs = await loadConversation(sessions, conv.id);
|
|
3442
3581
|
}
|
|
3443
3582
|
catch (err) {
|
|
@@ -3507,6 +3646,42 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3507
3646
|
resetTranscript,
|
|
3508
3647
|
wakeGoalDriver,
|
|
3509
3648
|
]);
|
|
3649
|
+
const restoreArchivedConversation = useCallback(async (conv) => {
|
|
3650
|
+
const sessions = sessionsRef.current;
|
|
3651
|
+
if (!sessions) {
|
|
3652
|
+
setPhase('ready');
|
|
3653
|
+
return;
|
|
3654
|
+
}
|
|
3655
|
+
if (state !== 'idle' ||
|
|
3656
|
+
abortRef.current !== null ||
|
|
3657
|
+
hasUnsettledTurn() ||
|
|
3658
|
+
queuedRef.current.length > 0 ||
|
|
3659
|
+
compactingRef.current ||
|
|
3660
|
+
exportingRef.current) {
|
|
3661
|
+
setPhase('ready');
|
|
3662
|
+
pushMessage('system', 'Finish the current turn and queued work before restoring a conversation.');
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
resumeCommittedRef.current = true;
|
|
3666
|
+
try {
|
|
3667
|
+
await unarchiveConversation(sessions, conv.id);
|
|
3668
|
+
}
|
|
3669
|
+
catch (err) {
|
|
3670
|
+
resumeCommittedRef.current = false;
|
|
3671
|
+
setPhase('ready');
|
|
3672
|
+
pushMessage('system', err instanceof ConversationIndexRefreshError
|
|
3673
|
+
? err.message
|
|
3674
|
+
: `Could not restore: ${err instanceof Error ? err.message : String(err)}`);
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3677
|
+
pushMessage('system', `Restored: ${conv.title}`);
|
|
3678
|
+
try {
|
|
3679
|
+
await resumeConversation(conv);
|
|
3680
|
+
}
|
|
3681
|
+
finally {
|
|
3682
|
+
resumeCommittedRef.current = false;
|
|
3683
|
+
}
|
|
3684
|
+
}, [hasUnsettledTurn, pushMessage, resumeConversation, state]);
|
|
3510
3685
|
/**
|
|
3511
3686
|
* Start a new conversation without deleting the one being left.
|
|
3512
3687
|
*
|
|
@@ -3961,6 +4136,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
3961
4136
|
...question.options.map((option) => ({
|
|
3962
4137
|
label: option.label,
|
|
3963
4138
|
description: option.description ?? '',
|
|
4139
|
+
// A badge, not label text: the SDK already took any
|
|
4140
|
+
// "(Recommended)" the model wrote out of the label.
|
|
4141
|
+
...(option.recommended === true ? { recommended: true } : {}),
|
|
3964
4142
|
})),
|
|
3965
4143
|
...(question.allowFreeText
|
|
3966
4144
|
? [{ label: 'Something else…', description: 'Answer in your own words' }]
|
|
@@ -4433,7 +4611,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4433
4611
|
if (closing)
|
|
4434
4612
|
pushMessage('system', closing, false, '✻', undefined, theme.text.muted);
|
|
4435
4613
|
}
|
|
4436
|
-
|
|
4614
|
+
// A save the operator asked for in words takes the place of
|
|
4615
|
+
// the proposal to save it.
|
|
4616
|
+
if (!triggerFollowUpsRef.current?.(st, st.completed ? 'completed' : 'stopped'))
|
|
4617
|
+
offerSkillSuggestion(st);
|
|
4437
4618
|
break;
|
|
4438
4619
|
}
|
|
4439
4620
|
case 'paused':
|
|
@@ -4445,6 +4626,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4445
4626
|
st.outcome = 'stopped';
|
|
4446
4627
|
st.queuePauseOutcome = 'paused';
|
|
4447
4628
|
st.notification = { kind: 'turn-settled', outcome: 'stopped' };
|
|
4629
|
+
triggerFollowUpsRef.current?.(st, 'paused');
|
|
4448
4630
|
if (event.handoff) {
|
|
4449
4631
|
// Nothing failed and nothing needs approving: a tool needs the
|
|
4450
4632
|
// operator to do something first. Continuing is one key.
|
|
@@ -4459,12 +4641,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4459
4641
|
`${describeTurnInterruption(event)}\nWhen that is done, press Enter to continue · Esc to stop.`, false, '‖');
|
|
4460
4642
|
setHandoffPark({ turnId: event.turnId, reason: event.handoff.reason });
|
|
4461
4643
|
}
|
|
4462
|
-
else
|
|
4463
|
-
pushMessage('system', describeTurnInterruption(event), false, '‖');
|
|
4644
|
+
else {
|
|
4645
|
+
pushMessage('system', `${describeTurnInterruption(event)}\nUse /resume to continue this checkpoint on the selected provider, or /abandon to start a new turn.`, false, '‖');
|
|
4646
|
+
}
|
|
4464
4647
|
break;
|
|
4465
4648
|
case 'error':
|
|
4466
4649
|
closeAssistant();
|
|
4467
4650
|
st.outcome = event.message === 'aborted' ? 'cancelled' : 'failed';
|
|
4651
|
+
triggerFollowUpsRef.current?.(st, st.outcome);
|
|
4468
4652
|
if (event.message !== 'aborted') {
|
|
4469
4653
|
st.notification = { kind: 'turn-settled', outcome: 'failed' };
|
|
4470
4654
|
pushMessage('system', describeTurnInterruption(event), false, '!');
|
|
@@ -4581,11 +4765,29 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4581
4765
|
// Reserved before the turn begins, so everything that refers to this
|
|
4582
4766
|
// turn — its goal-round authority above all — names it from the start.
|
|
4583
4767
|
const turnId = generateTurnId();
|
|
4768
|
+
// The composer triggers the operator armed for this message, checked
|
|
4769
|
+
// again now: the mode or the session may have changed while it waited
|
|
4770
|
+
// in the queue. None of them changes the operator's words.
|
|
4771
|
+
const requestedTriggers = prompt.kind === 'human' ? (prompt.triggers ?? []) : [];
|
|
4772
|
+
const triggerContext = triggerContextNow();
|
|
4773
|
+
const turnTriggers = requestedTriggers.filter((id) => triggerUnavailable(id, triggerContext) === undefined);
|
|
4774
|
+
const pinnedEffort = triggerEffort(turnTriggers, session.reasoningEffortLevels);
|
|
4584
4775
|
if (goalRound) {
|
|
4585
4776
|
pushMessage('system', `Goal round ${goalRound.round} / ${goalRound.maxGoalRounds}`, false, '◎', [`Objective: ${goalRound.objective}`]);
|
|
4586
4777
|
}
|
|
4587
4778
|
else {
|
|
4588
4779
|
pushMessage('user', text, false, undefined, undefined, undefined, humanPromptMeta(attached.length, attachments));
|
|
4780
|
+
const tagLine = transcriptTagLine(turnTriggers, {
|
|
4781
|
+
effort: pinnedEffort,
|
|
4782
|
+
steered: false,
|
|
4783
|
+
});
|
|
4784
|
+
if (tagLine)
|
|
4785
|
+
pushMessage('system', tagLine, false, '✦', undefined, theme.accent.trigger);
|
|
4786
|
+
for (const id of requestedTriggers) {
|
|
4787
|
+
const reason = triggerUnavailable(id, triggerContext);
|
|
4788
|
+
if (reason)
|
|
4789
|
+
pushMessage('system', `${triggerDefinition(id).label} · not applied: ${reason}`, false, '✧', undefined, theme.text.muted);
|
|
4790
|
+
}
|
|
4589
4791
|
}
|
|
4590
4792
|
// A plan is scoped to the request it was made for. Cleared here, at
|
|
4591
4793
|
// the start of the next one, rather than when the previous turn
|
|
@@ -4617,6 +4819,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4617
4819
|
startedInPlanMode: permissionModeRef.current === 'plan',
|
|
4618
4820
|
}
|
|
4619
4821
|
: {}),
|
|
4822
|
+
followUps: turnTriggers.filter((id) => triggerDefinition(id).scope === 'after-turn'),
|
|
4823
|
+
contextTexts: triggerContextTexts(turnTriggers),
|
|
4620
4824
|
};
|
|
4621
4825
|
liveStreamRef.current = st;
|
|
4622
4826
|
// A skill activated with /skills <name> stays in the prompt of every
|
|
@@ -4792,7 +4996,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4792
4996
|
activeTurnInboxRef.current = inbox;
|
|
4793
4997
|
const turnPermissionMode = permissionModeRef.current;
|
|
4794
4998
|
const turnReasoningEffort = reasoningEffortRef.current;
|
|
4795
|
-
const
|
|
4999
|
+
const turnHypermode = hypermodeRef.current;
|
|
4796
5000
|
const turnLimits = resolveTurnGuards(ctxRef.current.limits, turnLimitsOverrideRef.current);
|
|
4797
5001
|
// Always carry the guarded callback. `auto` and `strict` decide before
|
|
4798
5002
|
// calling it in makeResumeHandler; retaining it is what lets a session
|
|
@@ -4840,8 +5044,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
4840
5044
|
// while this turn runs, and the change governs what follows.
|
|
4841
5045
|
currentPermissionMode: () => permissionModeRef.current,
|
|
4842
5046
|
limits: turnLimits,
|
|
4843
|
-
|
|
4844
|
-
|
|
5047
|
+
// A trigger that pins the highest effort does so for this
|
|
5048
|
+
// turn only; the session's own setting is untouched.
|
|
5049
|
+
...((pinnedEffort ?? turnReasoningEffort) !== undefined
|
|
5050
|
+
? { effort: pinnedEffort ?? turnReasoningEffort }
|
|
5051
|
+
: {}),
|
|
5052
|
+
...(turnHypermode ? { hypermode: true } : {}),
|
|
5053
|
+
// Read at every iteration, so an after-turn trigger a steer
|
|
5054
|
+
// binds to this turn is said from the next one on.
|
|
5055
|
+
hostContext: () => st.contextTexts ?? [],
|
|
4845
5056
|
...(goalRound ? { goalRound } : {}),
|
|
4846
5057
|
// The mode above decides whether this callback is consulted.
|
|
4847
5058
|
onPermission: askPermission,
|
|
@@ -5055,6 +5266,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5055
5266
|
sendTerminalNotification,
|
|
5056
5267
|
session,
|
|
5057
5268
|
setQueuePause,
|
|
5269
|
+
triggerContextNow,
|
|
5058
5270
|
wakeGoalDriver,
|
|
5059
5271
|
]);
|
|
5060
5272
|
const selectModelIntent = useCallback(async (query) => {
|
|
@@ -5176,7 +5388,31 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5176
5388
|
pushMessage('system', `Could not save memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
5177
5389
|
}
|
|
5178
5390
|
}, [ctx.cwd, pushMessage, session]);
|
|
5179
|
-
|
|
5391
|
+
/**
|
|
5392
|
+
* Enter steered a message into the running turn. Its after-turn triggers
|
|
5393
|
+
* (save as skill) bind to that turn and are told to the model from its next
|
|
5394
|
+
* iteration; its turn triggers do not apply to a turn already running —
|
|
5395
|
+
* the tag row said so before Enter, and Tab would have queued it instead.
|
|
5396
|
+
*/
|
|
5397
|
+
const bindSteeredTriggers = useCallback((ids) => {
|
|
5398
|
+
if (ids.length === 0)
|
|
5399
|
+
return;
|
|
5400
|
+
const live = liveStreamRef.current;
|
|
5401
|
+
const afterTurn = ids.filter((id) => triggerDefinition(id).scope === 'after-turn');
|
|
5402
|
+
const turnOnly = ids.filter((id) => triggerDefinition(id).scope === 'turn');
|
|
5403
|
+
if (live && afterTurn.length > 0) {
|
|
5404
|
+
live.followUps = [...new Set([...(live.followUps ?? []), ...afterTurn])];
|
|
5405
|
+
live.contextTexts = [
|
|
5406
|
+
...new Set([...(live.contextTexts ?? []), ...triggerContextTexts(afterTurn)]),
|
|
5407
|
+
];
|
|
5408
|
+
const line = transcriptTagLine(afterTurn, { effort: undefined, steered: true });
|
|
5409
|
+
if (line)
|
|
5410
|
+
pushMessage('system', line, false, '✦', undefined, theme.accent.trigger);
|
|
5411
|
+
}
|
|
5412
|
+
if (turnOnly.length > 0)
|
|
5413
|
+
pushMessage('system', `${turnOnly.map((id) => triggerDefinition(id).label).join(', ')} · not applied: Enter steered this message into the running turn. Tab queues a message as a new turn with it.`, false, '✧', undefined, theme.text.muted);
|
|
5414
|
+
}, [pushMessage]);
|
|
5415
|
+
const handleSubmit = useCallback((value, attachments, mode, meta) => {
|
|
5180
5416
|
if (goalCommandInFlightRef.current) {
|
|
5181
5417
|
pushMessage('system', 'A goal command is still reaching durable session state. Wait for its result before sending another command or prompt.');
|
|
5182
5418
|
return;
|
|
@@ -5200,8 +5436,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5200
5436
|
}
|
|
5201
5437
|
// The operator moved on; a parked turn is still there for /resume.
|
|
5202
5438
|
setHandoffPark(null);
|
|
5203
|
-
|
|
5204
|
-
|
|
5439
|
+
// A loop is not something the operator typed here, so it is not a line
|
|
5440
|
+
// Up should bring back.
|
|
5441
|
+
const fromLoop = meta.source === 'operator-loop' || meta.source === 'model-loop';
|
|
5442
|
+
if (!fromLoop)
|
|
5443
|
+
setHistory((prev) => [...prev, value]);
|
|
5444
|
+
// What the model wrote is never the operator acting. A loop the model
|
|
5445
|
+
// made (`session_loop`) fires this path on a timer, for up to a week,
|
|
5446
|
+
// after one review of its creation — so its text must not reach the
|
|
5447
|
+
// host-side meanings of `/`, `!`, `#` or a model switch: `!` runs on
|
|
5448
|
+
// the host outside the sandbox with no review, and `#` writes memory
|
|
5449
|
+
// every later turn reads. It is sent as the prompt it looks like.
|
|
5450
|
+
const operatorText = meta.source !== 'model-loop';
|
|
5451
|
+
const selectionIntent = operatorText && !attachments?.length ? parseModelSelectionIntent(value) : undefined;
|
|
5205
5452
|
if (selectionIntent) {
|
|
5206
5453
|
void selectModelIntent(selectionIntent.query);
|
|
5207
5454
|
return;
|
|
@@ -5209,12 +5456,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5209
5456
|
// `#` remembers, `!` runs — neither is a prompt. Both are the
|
|
5210
5457
|
// operator acting directly, the way other coding agents spell it,
|
|
5211
5458
|
// and both leave a row the model reads on its next turn.
|
|
5212
|
-
if (value.startsWith('#') && value.slice(1).trim().length > 0) {
|
|
5459
|
+
if (operatorText && value.startsWith('#') && value.slice(1).trim().length > 0) {
|
|
5213
5460
|
const note = value.slice(1).trim();
|
|
5214
5461
|
void rememberProjectNote(note);
|
|
5215
5462
|
return;
|
|
5216
5463
|
}
|
|
5217
|
-
const escaped = shellEscapeCommand(value);
|
|
5464
|
+
const escaped = operatorText ? shellEscapeCommand(value) : null;
|
|
5218
5465
|
if (escaped !== null) {
|
|
5219
5466
|
pushMessage('user', value);
|
|
5220
5467
|
const rowId = pushMessage('tool', `! ${escaped}`, true, '…');
|
|
@@ -5235,13 +5482,33 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5235
5482
|
});
|
|
5236
5483
|
return;
|
|
5237
5484
|
}
|
|
5485
|
+
// Composer triggers (`./triggers/`). Only the composer's own submit
|
|
5486
|
+
// carries them, so nothing a loop, a picker or the model sends can
|
|
5487
|
+
// arm one. A message that is nothing but a trigger with a command of
|
|
5488
|
+
// its own (`bunu skill olarak kaydet`) runs that command here, in this
|
|
5489
|
+
// same call, so history holds the operator's words once; a trigger
|
|
5490
|
+
// that needs a task and got none is not applied, and says so.
|
|
5491
|
+
let armedTriggers = meta.source === 'composer' ? meta.triggers.map((trigger) => trigger.id) : [];
|
|
5492
|
+
let dispatch = value;
|
|
5493
|
+
if (meta.source === 'composer' && meta.onlyTriggers && armedTriggers.length > 0) {
|
|
5494
|
+
const command = armedTriggers
|
|
5495
|
+
.map((id) => triggerDefinition(id).standalone)
|
|
5496
|
+
.find((standalone) => standalone !== undefined);
|
|
5497
|
+
const needTask = armedTriggers.filter((id) => triggerDefinition(id).standalone === undefined);
|
|
5498
|
+
if (needTask.length > 0)
|
|
5499
|
+
pushMessage('system', `${needTask.map((id) => triggerDefinition(id).label).join(', ')} · not applied: the message has no task. Put the task in the same message${needTask.includes('hypermode') ? ', e.g. “hypermode fix the flaky test”' : ''}.`, false, '✧', undefined, theme.text.muted);
|
|
5500
|
+
armedTriggers = [];
|
|
5501
|
+
if (!command)
|
|
5502
|
+
return;
|
|
5503
|
+
dispatch = command;
|
|
5504
|
+
}
|
|
5238
5505
|
// What actually gets sent. A `prompt` action replaces it with text the
|
|
5239
5506
|
// command composed, and then takes the ordinary send path below —
|
|
5240
5507
|
// including the queue — so a command-driven turn is not a second way
|
|
5241
5508
|
// to run one.
|
|
5242
|
-
let outgoing =
|
|
5509
|
+
let outgoing = dispatch;
|
|
5243
5510
|
let skillFlow = false;
|
|
5244
|
-
const slash = runSlash(
|
|
5511
|
+
const slash = operatorText ? runSlash(dispatch, slashCtx, hostCommands) : null;
|
|
5245
5512
|
if (slash) {
|
|
5246
5513
|
switch (slash.kind) {
|
|
5247
5514
|
case 'message':
|
|
@@ -5380,6 +5647,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5380
5647
|
description: `${session?.webSearchSummary ?? 'Not resolved'} · view tools, workspace and usage.`,
|
|
5381
5648
|
},
|
|
5382
5649
|
{ name: 'setup', label: 'Provider setup', description: 'Check installed CLIs and access; connect or install without leaving Namzu.' },
|
|
5650
|
+
{
|
|
5651
|
+
name: `config triggers ${composerTriggersOn ? 'off' : 'on'}`,
|
|
5652
|
+
label: 'Composer triggers',
|
|
5653
|
+
description: composerTriggersOn
|
|
5654
|
+
? 'On · hypermode and save-as-skill words act for their message. Select to turn off.'
|
|
5655
|
+
: 'Off · words in a message are only words. Select to turn on.',
|
|
5656
|
+
},
|
|
5383
5657
|
{
|
|
5384
5658
|
name: 'config sources',
|
|
5385
5659
|
label: 'Setting sources',
|
|
@@ -5397,7 +5671,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5397
5671
|
description: command.description,
|
|
5398
5672
|
disabledReason: command.problem,
|
|
5399
5673
|
})),
|
|
5400
|
-
windowSize:
|
|
5674
|
+
windowSize: 8,
|
|
5401
5675
|
});
|
|
5402
5676
|
return;
|
|
5403
5677
|
}
|
|
@@ -5631,7 +5905,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5631
5905
|
const values = [undefined, ...levels];
|
|
5632
5906
|
const current = reasoningEffortRef.current;
|
|
5633
5907
|
setSelectedChoice(Math.max(0, values.findIndex((value) => value === current)));
|
|
5634
|
-
setChoicePicker(reasoningEffortPicker(session, current, false,
|
|
5908
|
+
setChoicePicker(reasoningEffortPicker(session, current, false, hypermodeRef.current) ?? null);
|
|
5635
5909
|
return;
|
|
5636
5910
|
}
|
|
5637
5911
|
case 'login':
|
|
@@ -5645,6 +5919,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5645
5919
|
const choices = [
|
|
5646
5920
|
...(readStoredSubscriptionCredential() ? ['anthropic'] : []),
|
|
5647
5921
|
...(readStoredCodexCredential() ? ['codex'] : []),
|
|
5922
|
+
...(readStoredGoogleApiKey() ? ['google'] : []),
|
|
5648
5923
|
];
|
|
5649
5924
|
if (choices.length === 0) {
|
|
5650
5925
|
removeStoredCredential('all');
|
|
@@ -5657,14 +5932,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5657
5932
|
setSelectedChoice(0);
|
|
5658
5933
|
setChoicePicker({
|
|
5659
5934
|
kind: 'credential-logout',
|
|
5660
|
-
title: 'Choose a stored
|
|
5935
|
+
title: 'Choose a stored credential to remove',
|
|
5661
5936
|
notice: 'Only credentials created by Namzu are listed. Device sessions owned by other tools are left alone.',
|
|
5662
5937
|
values: choices,
|
|
5663
5938
|
options: choices.map((choice) => ({
|
|
5664
|
-
label: choice === 'anthropic' ? 'Claude' : 'Codex',
|
|
5939
|
+
label: choice === 'anthropic' ? 'Claude' : choice === 'codex' ? 'Codex' : 'Gemini API key',
|
|
5665
5940
|
description: choice === 'anthropic'
|
|
5666
5941
|
? 'Remove only Namzu’s Claude subscription.'
|
|
5667
|
-
:
|
|
5942
|
+
: choice === 'codex'
|
|
5943
|
+
? 'Remove only Namzu’s Codex subscription.'
|
|
5944
|
+
: 'Remove only Namzu’s saved Gemini API key.',
|
|
5668
5945
|
})),
|
|
5669
5946
|
});
|
|
5670
5947
|
return;
|
|
@@ -5821,12 +6098,80 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
5821
6098
|
case 'resume':
|
|
5822
6099
|
void doResume();
|
|
5823
6100
|
return;
|
|
6101
|
+
case 'unarchive':
|
|
6102
|
+
void doUnarchive();
|
|
6103
|
+
return;
|
|
5824
6104
|
case 'title':
|
|
5825
6105
|
void doTitle(slash.title, slash.clear);
|
|
5826
6106
|
return;
|
|
5827
6107
|
case 'fork':
|
|
5828
6108
|
void doFork();
|
|
5829
6109
|
return;
|
|
6110
|
+
case 'worktree': {
|
|
6111
|
+
const [verb = 'list', label, extra] = slash.args;
|
|
6112
|
+
const notice = (content) => pushMessage('system', terminalDisplayText(content));
|
|
6113
|
+
if (!((verb === 'list' && label === undefined) ||
|
|
6114
|
+
(verb === 'create' && extra === undefined) ||
|
|
6115
|
+
(verb === 'fork' && extra === undefined) ||
|
|
6116
|
+
(verb === 'resume' &&
|
|
6117
|
+
label !== undefined &&
|
|
6118
|
+
extra === undefined))) {
|
|
6119
|
+
pushMessage('system', 'Usage: /worktree [list|create [name]|fork [name]|resume <name>]');
|
|
6120
|
+
return;
|
|
6121
|
+
}
|
|
6122
|
+
if (verb === 'fork' &&
|
|
6123
|
+
(abortRef.current ||
|
|
6124
|
+
hasUnsettledTurn() ||
|
|
6125
|
+
queuedRef.current.length > 0)) {
|
|
6126
|
+
pushMessage('system', 'Wait for the current turn and queued prompts to finish before forking to a worktree.');
|
|
6127
|
+
return;
|
|
6128
|
+
}
|
|
6129
|
+
if (conversationMutationRef.current)
|
|
6130
|
+
return;
|
|
6131
|
+
if (verb === 'fork') {
|
|
6132
|
+
conversationMutationRef.current = 'fork';
|
|
6133
|
+
setConversationMutation('fork');
|
|
6134
|
+
}
|
|
6135
|
+
void (async () => {
|
|
6136
|
+
try {
|
|
6137
|
+
const manager = await openManagedWorktrees(ctx.cwd);
|
|
6138
|
+
if (verb === 'list') {
|
|
6139
|
+
const items = await manager.list();
|
|
6140
|
+
notice(items.length === 0
|
|
6141
|
+
? 'No managed worktrees in this repository.'
|
|
6142
|
+
: items
|
|
6143
|
+
.map((item) => `${item.label}: ${item.path}${item.dirty ? ' (uncommitted files)' : ''}`)
|
|
6144
|
+
.join('\n'));
|
|
6145
|
+
}
|
|
6146
|
+
else if (verb === 'create') {
|
|
6147
|
+
const created = await manager.create(label);
|
|
6148
|
+
notice(`Created ${created.branch}. Open it in a new terminal: ${worktreeOpenHint(created.path)}${created.sourceDirty ? '\nUncommitted files stayed in this checkout.' : ''}`);
|
|
6149
|
+
}
|
|
6150
|
+
else if (verb === 'fork') {
|
|
6151
|
+
const scope = await materializeConversation();
|
|
6152
|
+
if (!scope)
|
|
6153
|
+
throw new Error('Conversation history is unavailable in this folder.');
|
|
6154
|
+
await persistenceTailRef.current;
|
|
6155
|
+
const forked = await manager.fork(scope.sessionId, label);
|
|
6156
|
+
notice(`Forked ${forked.copied} messages into ${forked.title}. Open the new checkout in a new terminal: ${worktreeOpenHint(forked.path, forked.conversationId)}${forked.sourceDirty ? '\nUncommitted files stayed in this checkout.' : ''}`);
|
|
6157
|
+
}
|
|
6158
|
+
else if (label) {
|
|
6159
|
+
const target = await manager.resume(label);
|
|
6160
|
+
notice(`Open ${label} in a new terminal: ${worktreeOpenHint(target.worktree.path, target.conversationId)}`);
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
catch (error) {
|
|
6164
|
+
notice(`Worktree: ${error instanceof Error ? error.message : String(error)}`);
|
|
6165
|
+
}
|
|
6166
|
+
finally {
|
|
6167
|
+
if (verb === 'fork') {
|
|
6168
|
+
conversationMutationRef.current = null;
|
|
6169
|
+
setConversationMutation(null);
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
})();
|
|
6173
|
+
return;
|
|
6174
|
+
}
|
|
5830
6175
|
case 'restore':
|
|
5831
6176
|
void doRestore(slash.turn);
|
|
5832
6177
|
return;
|
|
@@ -6153,9 +6498,25 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6153
6498
|
: 'Raw output mode off — rich transcript rendering restored.');
|
|
6154
6499
|
return;
|
|
6155
6500
|
}
|
|
6156
|
-
case '
|
|
6157
|
-
const
|
|
6158
|
-
|
|
6501
|
+
case 'composer-triggers': {
|
|
6502
|
+
const profile = ctx.configDebug?.selectedProfile?.name;
|
|
6503
|
+
const cascade = { cwd: ctx.cwd, ...(profile ? { profile } : {}) };
|
|
6504
|
+
if (slash.setting === 'list') {
|
|
6505
|
+
pushMessage('system', describeComposerTriggers(composerTriggersOn, cascade));
|
|
6506
|
+
return;
|
|
6507
|
+
}
|
|
6508
|
+
const on = slash.setting === 'on';
|
|
6509
|
+
// The session follows the choice whatever the files say; the
|
|
6510
|
+
// message says whether a later start will too.
|
|
6511
|
+
setComposerTriggersOverride(on);
|
|
6512
|
+
pushMessage('system', setComposerTriggers(on, cascade));
|
|
6513
|
+
return;
|
|
6514
|
+
}
|
|
6515
|
+
case 'hypermode': {
|
|
6516
|
+
if (slash.via === 'orchestrate')
|
|
6517
|
+
pushMessage('system', ORCHESTRATE_ALIAS_NOTICE);
|
|
6518
|
+
const enabled = slash.enabled === 'toggle' ? !hypermodeRef.current : slash.enabled;
|
|
6519
|
+
applyHypermode(enabled);
|
|
6159
6520
|
return;
|
|
6160
6521
|
}
|
|
6161
6522
|
case 'export-picker': {
|
|
@@ -6225,6 +6586,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6225
6586
|
text: outgoing,
|
|
6226
6587
|
...(attachments && attachments.length > 0 ? { attachments: [...attachments] } : {}),
|
|
6227
6588
|
...(skillFlow ? { skillFlow: true } : {}),
|
|
6589
|
+
...(armedTriggers.length > 0 && !skillFlow ? { triggers: armedTriggers } : {}),
|
|
6228
6590
|
};
|
|
6229
6591
|
// `/skills save` and `/skills new` are turns of their own: steered into
|
|
6230
6592
|
// a running turn, they would lose the flag that keeps the skill-making
|
|
@@ -6242,8 +6604,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6242
6604
|
message: createUserMessage(expanded.sendText, prompt.attachments),
|
|
6243
6605
|
attachedFiles: expanded.attached.length,
|
|
6244
6606
|
queueBoundary: queuedRef.current.length,
|
|
6245
|
-
}))
|
|
6607
|
+
})) {
|
|
6608
|
+
bindSteeredTriggers(armedTriggers);
|
|
6246
6609
|
return;
|
|
6610
|
+
}
|
|
6247
6611
|
}
|
|
6248
6612
|
}
|
|
6249
6613
|
enqueueQueued(prompt);
|
|
@@ -6251,17 +6615,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6251
6615
|
activeSkills,
|
|
6252
6616
|
advanceQueueContinuation,
|
|
6253
6617
|
applyPermissionMode,
|
|
6618
|
+
bindSteeredTriggers,
|
|
6619
|
+
composerTriggersOn,
|
|
6254
6620
|
conversationLearning,
|
|
6255
6621
|
applyReasoningEffort,
|
|
6256
6622
|
appLifetime,
|
|
6257
6623
|
ctx.cwd,
|
|
6258
6624
|
doResume,
|
|
6625
|
+
doUnarchive,
|
|
6259
6626
|
enqueueQueued,
|
|
6260
6627
|
exitWithSummary,
|
|
6261
6628
|
hasUnsettledTurn,
|
|
6262
6629
|
goalActivation,
|
|
6263
6630
|
goalStatus,
|
|
6264
6631
|
hostCommands,
|
|
6632
|
+
materializeConversation,
|
|
6265
6633
|
nextId,
|
|
6266
6634
|
openAgentCockpit,
|
|
6267
6635
|
runAgentsCommand,
|
|
@@ -6287,7 +6655,48 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6287
6655
|
// picker is also used by review/export flows. Keep only this dispatch hop in
|
|
6288
6656
|
// a ref so selecting a help row re-enters the one ordinary slash-command
|
|
6289
6657
|
// path instead of growing a second command executor.
|
|
6290
|
-
commandPickerSubmitRef.current = handleSubmit;
|
|
6658
|
+
commandPickerSubmitRef.current = (command) => handleSubmit(command, undefined, 'submit', { source: 'command-picker' });
|
|
6659
|
+
// A turn's after-turn triggers, when it ends. Save as skill runs exactly the
|
|
6660
|
+
// prompt `/skills save` sends, at the front of the queue, only when the
|
|
6661
|
+
// turn completed, did tool work, did not already save a skill, and saving
|
|
6662
|
+
// is still possible now. Otherwise one row says why, and `/skills save`
|
|
6663
|
+
// still works. Returns whether the turn had one, so the proposal to save
|
|
6664
|
+
// it is not also printed.
|
|
6665
|
+
triggerFollowUpsRef.current = (st, ending) => {
|
|
6666
|
+
const followUps = st.followUps ?? [];
|
|
6667
|
+
st.followUps = [];
|
|
6668
|
+
if (!followUps.includes('save-skill'))
|
|
6669
|
+
return false;
|
|
6670
|
+
const reason = ending === 'cancelled'
|
|
6671
|
+
? 'the turn was cancelled'
|
|
6672
|
+
: ending === 'failed'
|
|
6673
|
+
? 'the turn failed'
|
|
6674
|
+
: ending === 'paused'
|
|
6675
|
+
? 'the turn paused'
|
|
6676
|
+
: ending === 'stopped'
|
|
6677
|
+
? 'the turn stopped before it finished'
|
|
6678
|
+
: !st.learning || st.learning.steps < 1
|
|
6679
|
+
? 'the turn did no tool work'
|
|
6680
|
+
: st.learning.savedSkill
|
|
6681
|
+
? 'the turn already saved a skill'
|
|
6682
|
+
: triggerUnavailable('save-skill', triggerContextNow());
|
|
6683
|
+
if (reason) {
|
|
6684
|
+
pushMessage('system', `save as skill · not run: ${reason} · /skills save runs it anyway`, false, '✧', undefined, theme.text.muted);
|
|
6685
|
+
return true;
|
|
6686
|
+
}
|
|
6687
|
+
// The proposal was taken up, as `/skills save` records it.
|
|
6688
|
+
conversationLearning().proposed = true;
|
|
6689
|
+
const home = resolveNamzuHome();
|
|
6690
|
+
const ledger = readSuggestionLedger(home);
|
|
6691
|
+
if (ledger.unanswered > 0)
|
|
6692
|
+
writeSuggestionLedger(home, { ...ledger, unanswered: 0 });
|
|
6693
|
+
replaceQueued([
|
|
6694
|
+
{ kind: 'human', text: learnSkillPrompt(), skillFlow: true },
|
|
6695
|
+
...queuedRef.current,
|
|
6696
|
+
]);
|
|
6697
|
+
pushMessage('system', 'save as skill · running /skills save for this turn', false, '✦', undefined, theme.accent.trigger);
|
|
6698
|
+
return true;
|
|
6699
|
+
};
|
|
6291
6700
|
saveSkillNotifyRef.current = () => sendTerminalNotification({ kind: 'approval-required' });
|
|
6292
6701
|
scheduleLiveRef.current = {
|
|
6293
6702
|
...(scheduleLiveRef.current?.model ? { model: scheduleLiveRef.current.model } : {}),
|
|
@@ -6297,7 +6706,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6297
6706
|
},
|
|
6298
6707
|
ask: askQuestion,
|
|
6299
6708
|
askPermission: onPermission,
|
|
6300
|
-
submit: (text) => handleSubmit(text
|
|
6709
|
+
submit: (text, createdBy) => handleSubmit(text, undefined, 'submit', {
|
|
6710
|
+
source: createdBy === 'operator' ? 'operator-loop' : 'model-loop',
|
|
6711
|
+
}),
|
|
6301
6712
|
};
|
|
6302
6713
|
// Scheduled work since the TUI last looked: one line, after the first
|
|
6303
6714
|
// paint, never blocking it. Loops fire when a turn ends, and on their timer.
|
|
@@ -6543,29 +6954,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6543
6954
|
announce: true,
|
|
6544
6955
|
chooseReasoningEffort: options.chooseReasoningEffort,
|
|
6545
6956
|
});
|
|
6957
|
+
return !signal.aborted;
|
|
6546
6958
|
}
|
|
6547
6959
|
catch (err) {
|
|
6548
6960
|
// A superseded choice no longer owns even its failure message. Its
|
|
6549
6961
|
// eventual session object is disposed inside `hydrateSession`; a live
|
|
6550
6962
|
// choice stays on the picker with the actionable construction error.
|
|
6551
6963
|
if (signal.aborted)
|
|
6552
|
-
return;
|
|
6964
|
+
return false;
|
|
6553
6965
|
if (options.revealAllOnFailure) {
|
|
6554
6966
|
setPickerDetected(null);
|
|
6555
6967
|
setPickerSelectionKind('provider-and-model');
|
|
6556
6968
|
setPickerInitialView('providers');
|
|
6557
6969
|
}
|
|
6558
6970
|
setPickerNotice(`Could not start the selected provider: ${err instanceof Error ? err.message : String(err)}`);
|
|
6971
|
+
return false;
|
|
6559
6972
|
}
|
|
6560
6973
|
}, [hydrateSession]);
|
|
6561
|
-
/**
|
|
6562
|
-
* A credential the operator typed. Held in memory for this process only.
|
|
6563
|
-
*
|
|
6564
|
-
* Deliberately does NOT call `writePreferences`: preferences are a file, and
|
|
6565
|
-
* the whole contract of this entry point is that nothing lands on disk. The
|
|
6566
|
-
* provider choice is not persisted either, because persisting it would leave
|
|
6567
|
-
* a preference pointing at a credential that will not exist next launch.
|
|
6568
|
-
*/
|
|
6974
|
+
/** A typed Google API key is saved only after its provider session opens. */
|
|
6569
6975
|
const handleTypedCredential = useCallback((credential, disposition, signal) => {
|
|
6570
6976
|
const next = [credential, ...detected.filter((d) => d.entry.id !== credential.entry.id)];
|
|
6571
6977
|
setDetected(next);
|
|
@@ -6581,7 +6987,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6581
6987
|
const prefs = saved && primaryProvider(saved).id === credential.entry.id
|
|
6582
6988
|
? saved
|
|
6583
6989
|
: selectPrimaryProvider(saved, { id: credential.entry.id });
|
|
6584
|
-
void
|
|
6990
|
+
void (async () => {
|
|
6991
|
+
const started = await hydrateFromPicker(prefs, next, signal);
|
|
6992
|
+
if (!started || credential.entry.id !== 'google')
|
|
6993
|
+
return;
|
|
6994
|
+
let path;
|
|
6995
|
+
try {
|
|
6996
|
+
path = writeStoredGoogleApiKey(credential.apiKey ?? '');
|
|
6997
|
+
}
|
|
6998
|
+
catch (error) {
|
|
6999
|
+
pushMessage('system', `Gemini is connected, but its API key could not be saved privately: ${error instanceof Error ? error.message : String(error)}. It is usable only in this session.`);
|
|
7000
|
+
return;
|
|
7001
|
+
}
|
|
7002
|
+
setDetected((current) => current.map((provider) => provider.entry.id === 'google' &&
|
|
7003
|
+
provider.source.kind === 'session' &&
|
|
7004
|
+
provider.apiKey === credential.apiKey
|
|
7005
|
+
? { ...provider, source: { kind: 'stored-gemini-key', path } }
|
|
7006
|
+
: provider));
|
|
7007
|
+
try {
|
|
7008
|
+
writePreferences(prefs);
|
|
7009
|
+
}
|
|
7010
|
+
catch (error) {
|
|
7011
|
+
pushMessage('system', `Gemini API key was saved privately at ${path}, but the provider preference could not be saved: ${error instanceof Error ? error.message : String(error)}. Run /logout gemini to remove the key.`);
|
|
7012
|
+
return;
|
|
7013
|
+
}
|
|
7014
|
+
pushMessage('system', `Gemini API key saved privately at ${path} for future launches. Run /logout gemini to remove it.`);
|
|
7015
|
+
})();
|
|
6585
7016
|
}, [detected, hydrateFromPicker, pushMessage]);
|
|
6586
7017
|
const handlePickerSubmit = useCallback((selection, signal) => {
|
|
6587
7018
|
const prefs = selectPrimaryProvider(savedPrefsRef.current, {
|
|
@@ -6688,7 +7119,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6688
7119
|
return;
|
|
6689
7120
|
}
|
|
6690
7121
|
// Resume picker owns the keyboard while open.
|
|
6691
|
-
if (phase === 'resume') {
|
|
7122
|
+
if (phase === 'resume' || phase === 'unarchive') {
|
|
6692
7123
|
// Once a conversation is being read, the keyboard does nothing here.
|
|
6693
7124
|
// The choice is already being acted on; a second Enter would start a
|
|
6694
7125
|
// second read and an Esc would hand back a screen that is about to be
|
|
@@ -6705,7 +7136,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
6705
7136
|
else if (key.return) {
|
|
6706
7137
|
const conv = resumeList[selectedResumeRef.current];
|
|
6707
7138
|
if (conv)
|
|
6708
|
-
void resumeConversation(conv);
|
|
7139
|
+
void (phase === 'unarchive' ? restoreArchivedConversation(conv) : resumeConversation(conv));
|
|
6709
7140
|
}
|
|
6710
7141
|
else if (key.escape || (key.ctrl && input === 'c'))
|
|
6711
7142
|
setPhase('ready');
|
|
@@ -7226,6 +7657,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7226
7657
|
const lifecycleOwnsViewport = outputViewer !== null ||
|
|
7227
7658
|
phase === 'trust' ||
|
|
7228
7659
|
phase === 'resume' ||
|
|
7660
|
+
phase === 'unarchive' ||
|
|
7229
7661
|
phase === 'edit' ||
|
|
7230
7662
|
phase === 'picker' ||
|
|
7231
7663
|
agentSurface !== null;
|
|
@@ -7292,6 +7724,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7292
7724
|
: `Session: ${permissionSource}`
|
|
7293
7725
|
: undefined;
|
|
7294
7726
|
const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
|
|
7727
|
+
const highestEffort = highestReasoningEffort(session?.reasoningEffortLevels);
|
|
7728
|
+
const hypermodeLevel = hypermodeEffort(session?.reasoningEffortLevels);
|
|
7729
|
+
const composerTriggers = composerTriggersOn
|
|
7730
|
+
? {
|
|
7731
|
+
registry: composerTriggerRegistry,
|
|
7732
|
+
suggest: composerTriggerSettings.suggest,
|
|
7733
|
+
context: {
|
|
7734
|
+
permissionMode: displayedPermissionMode,
|
|
7735
|
+
sessionHypermode: hypermode,
|
|
7736
|
+
effortMenu: highestEffort !== undefined,
|
|
7737
|
+
agentTool: true,
|
|
7738
|
+
skillCreator: skillCreatorAvailable,
|
|
7739
|
+
scheduleTool: true,
|
|
7740
|
+
},
|
|
7741
|
+
...(highestEffort ? { highestEffort } : {}),
|
|
7742
|
+
...(hypermodeLevel ? { hypermodeEffort: hypermodeLevel } : {}),
|
|
7743
|
+
}
|
|
7744
|
+
: undefined;
|
|
7295
7745
|
// True exactly when the phase ternary below renders its final branch — the
|
|
7296
7746
|
// normal composer view, as opposed to provider setup or a lifecycle
|
|
7297
7747
|
// picker/prompt. The agent rail is a feature of that view: computing this
|
|
@@ -7315,7 +7765,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7315
7765
|
setPickerSelectionKind('provider-and-model');
|
|
7316
7766
|
setPhase('picker');
|
|
7317
7767
|
void discoverProviders({ skipProbes: true }).then(setPickerDetected).catch((error) => setPickerNotice(String(error)));
|
|
7318
|
-
} })) : phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onSetup: () => setProviderSetup(true), onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice,
|
|
7768
|
+
} })) : phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : phase === 'resume' || phase === 'unarchive' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume, mode: phase === 'unarchive' ? 'restore' : 'resume' })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onSetup: () => setProviderSetup(true), onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice,
|
|
7319
7769
|
// The same condition `handlePickerCancel` branches on, read
|
|
7320
7770
|
// where it is decided rather than guessed on the screen: with no
|
|
7321
7771
|
// session behind the picker, esc exits namzu and the footer says
|
|
@@ -7332,7 +7782,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7332
7782
|
agentSurface === null &&
|
|
7333
7783
|
outputViewer === null &&
|
|
7334
7784
|
choicePicker?.kind === 'reasoning-effort' &&
|
|
7335
|
-
effortSlider ? (_jsx(EffortSlider, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, layout: effortSlider, columns: Math.max(1, (terminal.columns ?? 80) - 2)
|
|
7785
|
+
effortSlider ? (_jsx(EffortSlider, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, layout: effortSlider, columns: Math.max(1, (terminal.columns ?? 80) - 2) })) : permission === null && agentSurface === null && outputViewer === null && choicePicker ? (_jsx(ChoicePicker, { busy: 'busy' in choicePicker && choicePicker.busy === true, columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && outputViewer === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, ...(hypermode ? { mode: HYPERMODE } : {}), focus: phase === 'ready' &&
|
|
7336
7786
|
state !== 'awaiting-permission' &&
|
|
7337
7787
|
!compacting &&
|
|
7338
7788
|
externalEditorRequest === null &&
|
|
@@ -7360,7 +7810,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7360
7810
|
? queuePause.outcome === 'paused'
|
|
7361
7811
|
? 'held after a resumable turn paused; wait for recovery, change model, or send a message to release it'
|
|
7362
7812
|
: `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
7363
|
-
: 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
|
|
7813
|
+
: 'sending when ready', queuedTriggerLabels(queued)] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
|
|
7364
7814
|
phase !== 'ready' ||
|
|
7365
7815
|
state === 'awaiting-permission' ||
|
|
7366
7816
|
compacting ||
|
|
@@ -7381,7 +7831,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
|
|
|
7381
7831
|
(handoffPark !== null && state === 'idle'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, onOpenAgentPanel: openAgentCockpit, userCommands: userCommands, builtins: hostCommands.map((command) => ({
|
|
7382
7832
|
...command,
|
|
7383
7833
|
description: command.unavailable?.(slashCtx) ?? command.description,
|
|
7384
|
-
})), mentionCandidates: mentionCandidates, history: history })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort,
|
|
7834
|
+
})), mentionCandidates: mentionCandidates, history: history, ...(composerTriggers ? { triggers: composerTriggers } : {}), turnActive: !compacting && (state === 'thinking' || state === 'tool') })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort, hypermode: hypermode, goal: statusGoal, state: state, hint: statusHint, permissionMode: displayedPermissionMode, canCycleMode: true }), showComposerSurface &&
|
|
7385
7835
|
permission === null &&
|
|
7386
7836
|
agentSurface === null &&
|
|
7387
7837
|
outputViewer === null ? (_jsx(AgentNarrationBand, { lines: narration })) : null, showComposerSurface &&
|
|
@@ -7434,6 +7884,8 @@ hasDraft = false) {
|
|
|
7434
7884
|
return 'y trust this folder · n / esc exit';
|
|
7435
7885
|
if (phase === 'resume')
|
|
7436
7886
|
return '↑↓ navigate · enter resume · esc cancel';
|
|
7887
|
+
if (phase === 'unarchive')
|
|
7888
|
+
return '↑↓ navigate · enter restore · esc cancel';
|
|
7437
7889
|
if (phase === 'edit')
|
|
7438
7890
|
return '← older · → newer · enter fork and edit · esc cancel';
|
|
7439
7891
|
if (phase === 'probing')
|
|
@@ -7596,25 +8048,36 @@ function permissionPicker(mode, approvedAll, more = false) {
|
|
|
7596
8048
|
};
|
|
7597
8049
|
}
|
|
7598
8050
|
/**
|
|
7599
|
-
* The picker's sentinel for the
|
|
8051
|
+
* The picker's sentinel for the hypermode row, below the rule.
|
|
7600
8052
|
*
|
|
7601
8053
|
* A plain string outside the `ReasoningEffort` union by construction — never
|
|
7602
8054
|
* added to any model's published menu — so it can share the same picker
|
|
7603
8055
|
* without ever being mistaken for a level `/effort <token>` could select.
|
|
7604
8056
|
*/
|
|
7605
|
-
const
|
|
8057
|
+
const HYPERMODE_VALUE = 'hypermode';
|
|
8058
|
+
/** ` · ✦ save as skill` when queued messages carry composer triggers, so the queue line says so. */
|
|
8059
|
+
function queuedTriggerLabels(queued) {
|
|
8060
|
+
const labels = new Set();
|
|
8061
|
+
for (const prompt of queued) {
|
|
8062
|
+
if (prompt.kind !== 'human')
|
|
8063
|
+
continue;
|
|
8064
|
+
for (const id of prompt.triggers ?? [])
|
|
8065
|
+
labels.add(triggerDefinition(id).label);
|
|
8066
|
+
}
|
|
8067
|
+
return labels.size > 0 ? ` · ✦ ${[...labels].join(', ')}` : '';
|
|
8068
|
+
}
|
|
7606
8069
|
/** Last entry of a low-to-high menu every provider publishes in that order; `undefined` when none is known. */
|
|
7607
8070
|
function highestReasoningEffort(levels) {
|
|
7608
8071
|
return levels && levels.length > 0 ? levels[levels.length - 1] : undefined;
|
|
7609
8072
|
}
|
|
7610
|
-
function reasoningEffortPicker(session, current, afterModelSelection = false,
|
|
8073
|
+
function reasoningEffortPicker(session, current, afterModelSelection = false, hypermodeOn = false) {
|
|
7611
8074
|
const levels = session.reasoningEffortLevels;
|
|
7612
8075
|
if (!session.hasProvider || levels === undefined)
|
|
7613
8076
|
return undefined;
|
|
7614
8077
|
const effortValues = [undefined, ...levels];
|
|
7615
8078
|
const values = [
|
|
7616
8079
|
...effortValues,
|
|
7617
|
-
|
|
8080
|
+
HYPERMODE_VALUE,
|
|
7618
8081
|
];
|
|
7619
8082
|
return {
|
|
7620
8083
|
kind: 'reasoning-effort',
|
|
@@ -7635,12 +8098,11 @@ function reasoningEffortPicker(session, current, afterModelSelection = false, or
|
|
|
7635
8098
|
})),
|
|
7636
8099
|
{
|
|
7637
8100
|
// Below a rule, visually apart from the levels above: a session
|
|
7638
|
-
// setting, not a sixth level the provider published.
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
current: orchestrateOn,
|
|
8101
|
+
// setting, not a sixth level the provider published. The label
|
|
8102
|
+
// names the level it pins, then the mode.
|
|
8103
|
+
label: hypermodeStopLabel(hypermodeEffort(levels)),
|
|
8104
|
+
description: `${hypermodeOn ? 'On' : 'Off'} · ${HYPERMODE_SUMMARY}`,
|
|
8105
|
+
current: hypermodeOn,
|
|
7644
8106
|
ruleBefore: true,
|
|
7645
8107
|
},
|
|
7646
8108
|
],
|