@sayknow-cli/coding-agent 0.3.5 → 0.3.7
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/README.md +76 -0
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/telegram-cli.d.ts +13 -0
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/commands/telegram.d.ts +22 -0
- package/dist/types/config/keybindings.d.ts +12 -6
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +284 -4
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/config/telegram-autostart.d.ts +14 -0
- package/dist/types/config/telegram-env-bridge.d.ts +18 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/defaults/skc-defaults.d.ts +7 -0
- package/dist/types/discovery/cursor.d.ts +4 -1
- package/dist/types/discovery/index.d.ts +0 -1
- package/dist/types/discovery/windsurf.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/i18n/messages/en.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -1
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +5 -1
- package/dist/types/modes/components/skill-message.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +15 -3
- package/dist/types/modes/components/two-column-body.d.ts +9 -0
- package/dist/types/modes/components/welcome.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/input-controller.d.ts +6 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
- package/dist/types/modes/slash-command-visibility.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/types.d.ts +5 -1
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-daemon.d.ts +4 -10
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
- package/dist/types/runtime-mcp/config.d.ts +2 -0
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
- package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
- package/dist/types/skc-runtime/state-writer.d.ts +1 -0
- package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/slash-commands/types.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +7 -0
- package/dist/types/tools/skill.d.ts +1 -1
- package/dist/types/utils/cmux-workspace.d.ts +52 -0
- package/dist/types/web/search/index.d.ts +4 -2
- package/dist/types/web/search/provider.d.ts +14 -0
- package/dist/types/web/search/providers/utils.d.ts +42 -13
- package/dist/types/web/search/render.d.ts +1 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +3 -0
- package/scripts/verify-insane-vendor.ts +0 -5
- package/src/capability/mcp.ts +2 -0
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +4 -2
- package/src/cli/telegram-cli.ts +156 -0
- package/src/cli/update-cli.ts +179 -13
- package/src/cli/web-search-cli.ts +5 -7
- package/src/cli.ts +20 -1
- package/src/commands/launch.ts +47 -2
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/commands/ralplan.ts +1 -0
- package/src/commands/telegram.ts +47 -0
- package/src/config/keybindings.ts +15 -6
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-registry.ts +55 -1
- package/src/config/models-config-schema.ts +9 -0
- package/src/config/settings-schema.ts +256 -4
- package/src/config/settings.ts +11 -0
- package/src/config/telegram-autostart.ts +107 -0
- package/src/config/telegram-env-bridge.ts +92 -0
- package/src/coordinator-mcp/server.ts +276 -34
- package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
- package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
- package/src/defaults/skc-defaults.ts +16 -0
- package/src/discovery/builtin.ts +12 -0
- package/src/discovery/cursor.ts +5 -84
- package/src/discovery/gemini.ts +3 -89
- package/src/discovery/index.ts +4 -1
- package/src/discovery/mcp-json.ts +11 -0
- package/src/discovery/opencode.ts +5 -122
- package/src/discovery/windsurf.ts +4 -82
- package/src/edit/read-file.ts +9 -1
- package/src/extensibility/extensions/runner.ts +2 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/skills.ts +30 -2
- package/src/hooks/skill-state.ts +33 -73
- package/src/i18n/messages/de.ts +1 -0
- package/src/i18n/messages/en.ts +1 -0
- package/src/i18n/messages/es.ts +1 -0
- package/src/i18n/messages/fr.ts +1 -0
- package/src/i18n/messages/ja.settings.ts +28 -0
- package/src/i18n/messages/ja.ts +1 -0
- package/src/i18n/messages/ko.settings.ts +28 -0
- package/src/i18n/messages/ko.ts +1 -0
- package/src/i18n/messages/zh.settings.ts +28 -0
- package/src/i18n/messages/zh.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +12 -7
- package/src/main.ts +5 -2
- package/src/migrate/executor.ts +23 -6
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/components/agent-dashboard.ts +3 -35
- package/src/modes/components/custom-editor.ts +26 -0
- package/src/modes/components/custom-model-preset-wizard.ts +30 -189
- package/src/modes/components/extensions/extension-dashboard.ts +2 -48
- package/src/modes/components/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +246 -11
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/session-selector.ts +19 -9
- package/src/modes/components/settings-selector.ts +551 -11
- package/src/modes/components/skill-message.ts +18 -10
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/components/status-line/segments.ts +16 -19
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +281 -117
- package/src/modes/components/tree-selector.ts +28 -8
- package/src/modes/components/two-column-body.ts +36 -0
- package/src/modes/components/welcome.ts +165 -30
- package/src/modes/controllers/command-controller.ts +26 -2
- package/src/modes/controllers/event-controller.ts +102 -4
- package/src/modes/controllers/extension-ui-controller.ts +21 -2
- package/src/modes/controllers/input-controller.ts +214 -47
- package/src/modes/controllers/selector-controller.ts +35 -33
- package/src/modes/interactive-mode.ts +36 -30
- package/src/modes/prompt-action-autocomplete.ts +48 -8
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
- package/src/modes/slash-command-visibility.ts +9 -0
- package/src/modes/theme/theme.ts +5 -1
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/types.ts +8 -1
- package/src/modes/utils/hotkeys-markdown.ts +5 -2
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/modes/utils/ui-helpers.ts +76 -3
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/html-format.ts +75 -1
- package/src/notifications/index.ts +32 -2
- package/src/notifications/lifecycle-control-runtime.ts +12 -3
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-cli.ts +125 -13
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +88 -46
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/agents/architect.md +7 -4
- package/src/prompts/agents/critic.md +15 -6
- package/src/prompts/agents/planner.md +8 -3
- package/src/prompts/system/system-prompt.md +20 -2
- package/src/prompts/tools/bash.md +1 -1
- package/src/runtime-mcp/config-writer.ts +28 -0
- package/src/runtime-mcp/config.ts +7 -0
- package/src/runtime-mcp/loader.ts +3 -0
- package/src/runtime-mcp/manager.ts +3 -0
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk.ts +10 -11
- package/src/session/agent-session.ts +103 -13
- package/src/session/session-manager.ts +58 -18
- package/src/skc-runtime/launch-tmux.ts +34 -8
- package/src/skc-runtime/launch-worktree.ts +16 -2
- package/src/skc-runtime/ralplan-runtime.ts +22 -6
- package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
- package/src/skc-runtime/session-state-sidecar.ts +167 -24
- package/src/skc-runtime/state-runtime.ts +8 -5
- package/src/skc-runtime/state-writer.ts +5 -5
- package/src/skc-runtime/team-runtime.ts +21 -6
- package/src/skc-runtime/tmux-common.ts +16 -0
- package/src/skc-runtime/tmux-sessions.ts +13 -9
- package/src/skc-runtime/ultragoal-guard.ts +150 -24
- package/src/skc-runtime/ultragoal-runtime.ts +16 -124
- package/src/skc-runtime/workflow-manifest.generated.json +0 -8
- package/src/skc-runtime/workflow-manifest.ts +0 -1
- package/src/slash-commands/builtin-registry.ts +136 -23
- package/src/slash-commands/types.ts +2 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +5 -0
- package/src/tools/ast-grep.ts +10 -7
- package/src/tools/bash.ts +39 -14
- package/src/tools/index.ts +8 -1
- package/src/tools/skill.ts +1 -1
- package/src/utils/cmux-workspace.ts +233 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/kagi.ts +1 -1
- package/src/web/parallel.ts +2 -2
- package/src/web/search/index.ts +137 -52
- package/src/web/search/provider.ts +80 -0
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/brave.ts +1 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/providers/exa.ts +1 -1
- package/src/web/search/providers/gemini.ts +7 -3
- package/src/web/search/providers/jina.ts +1 -1
- package/src/web/search/providers/kimi.ts +6 -1
- package/src/web/search/providers/openai-compatible.ts +1 -1
- package/src/web/search/providers/parallel.ts +1 -1
- package/src/web/search/providers/perplexity.ts +2 -2
- package/src/web/search/providers/searxng.ts +1 -1
- package/src/web/search/providers/synthetic.ts +1 -1
- package/src/web/search/providers/tavily.ts +1 -1
- package/src/web/search/providers/utils.ts +67 -17
- package/src/web/search/providers/xai.ts +1 -1
- package/src/web/search/providers/zai.ts +1 -1
- package/src/web/search/render.ts +3 -0
- package/src/workflow/workflow-intent-diff.ts +199 -0
- package/vendor/insane-search/MANIFEST.json +1 -3
- package/vendor/insane-search/engine/tests/test_u1.py +0 -28
- package/vendor/insane-search/engine/transport.py +2 -3
- package/vendor/insane-search/engine/validators.py +0 -14
- package/src/defaults/skc/rules/ponytail.md +0 -68
package/src/main.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { activateModelProfile } from "./config/model-profile-activation";
|
|
|
30
30
|
import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
|
|
31
31
|
import { resolveCliModel, resolveModelRoleValue, resolveModelScope, type ScopedModel } from "./config/model-resolver";
|
|
32
32
|
import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
|
|
33
|
+
import { maybeAutostartTelegramRemote } from "./config/telegram-autostart";
|
|
33
34
|
import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory } from "./defaults/skc-grok-cli";
|
|
34
35
|
import { initializeWithSettings } from "./discovery";
|
|
35
36
|
import { exportFromFile } from "./export/html";
|
|
@@ -671,7 +672,7 @@ async function buildSessionOptions(
|
|
|
671
672
|
options.thinkingLevel = scopedModels[0].thinkingLevel;
|
|
672
673
|
}
|
|
673
674
|
|
|
674
|
-
// Scoped models for
|
|
675
|
+
// Scoped models for Alt+N cycling - fill in default thinking levels when not explicit
|
|
675
676
|
if (scopedModels.length > 0) {
|
|
676
677
|
const defaultThinkingLevel = activeSettings.get("defaultThinkingLevel");
|
|
677
678
|
options.scopedModels = scopedModels.map(scopedModel => ({
|
|
@@ -845,6 +846,8 @@ export async function runRootCommand(
|
|
|
845
846
|
|
|
846
847
|
// Initialize discovery system with settings for provider persistence
|
|
847
848
|
logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
|
|
849
|
+
// Auto-start Telegram Remote gateway if enabled in settings
|
|
850
|
+
if (!parsedArgs.print && !autoPrint) await maybeAutostartTelegramRemote();
|
|
848
851
|
|
|
849
852
|
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
|
|
850
853
|
const smolModel = parsedArgs.smol ?? $env.PI_SMOL_MODEL;
|
|
@@ -1067,7 +1070,7 @@ export async function runRootCommand(
|
|
|
1067
1070
|
return `${scopedModel.model.id}${thinkingStr}`;
|
|
1068
1071
|
})
|
|
1069
1072
|
.join(", ");
|
|
1070
|
-
process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(
|
|
1073
|
+
process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Alt+N to cycle)")}`)}\n`);
|
|
1071
1074
|
}
|
|
1072
1075
|
|
|
1073
1076
|
if ($env.PI_TIMING) {
|
package/src/migrate/executor.ts
CHANGED
|
@@ -12,11 +12,28 @@ import * as path from "node:path";
|
|
|
12
12
|
import { upsertMCPServer } from "../runtime-mcp/config-writer";
|
|
13
13
|
import type { MigrateAction } from "./types";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
function trustedSkillDestinationBase(destination: string): string {
|
|
16
|
+
const skillDir = path.dirname(path.resolve(destination));
|
|
17
|
+
const skillsRoot = path.dirname(skillDir);
|
|
18
|
+
const scopeRoot = path.dirname(skillsRoot);
|
|
19
|
+
return path.basename(scopeRoot) === ".skc" ? path.dirname(scopeRoot) : scopeRoot;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function ensureRealDirectoryPathNoFollow(directory: string, trustedBase: string): Promise<void> {
|
|
23
|
+
const resolvedBase = path.resolve(trustedBase);
|
|
24
|
+
const resolvedDirectory = path.resolve(directory);
|
|
25
|
+
const relative = path.relative(resolvedBase, resolvedDirectory);
|
|
26
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
27
|
+
throw new Error(`skill destination escapes trusted base: ${resolvedDirectory}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const baseStat = await fs.stat(resolvedBase);
|
|
31
|
+
if (!baseStat.isDirectory()) {
|
|
32
|
+
throw new Error(`skill destination base is not a directory: ${resolvedBase}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let current = resolvedBase;
|
|
36
|
+
for (const part of relative.split(path.sep).filter(part => part && part !== ".")) {
|
|
20
37
|
current = path.join(current, part);
|
|
21
38
|
try {
|
|
22
39
|
const stat = await fs.lstat(current);
|
|
@@ -36,7 +53,7 @@ async function ensureRealDirectoryPathNoFollow(directory: string): Promise<void>
|
|
|
36
53
|
|
|
37
54
|
async function writeSkillFileNoFollow(destination: string, content: string): Promise<void> {
|
|
38
55
|
const skillDir = path.dirname(destination);
|
|
39
|
-
await ensureRealDirectoryPathNoFollow(skillDir);
|
|
56
|
+
await ensureRealDirectoryPathNoFollow(skillDir, trustedSkillDestinationBase(destination));
|
|
40
57
|
const handle = await fs.open(
|
|
41
58
|
destination,
|
|
42
59
|
fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_TRUNC | fsSync.constants.O_NOFOLLOW,
|
|
@@ -710,9 +710,6 @@ export class AcpAgent implements Agent {
|
|
|
710
710
|
text: string,
|
|
711
711
|
options: { directAliasMayCollide?: boolean } = {},
|
|
712
712
|
): Promise<boolean> {
|
|
713
|
-
if (!text.startsWith("/")) {
|
|
714
|
-
return false;
|
|
715
|
-
}
|
|
716
713
|
if (!record.session.skillsSettings?.enableSkillCommands) {
|
|
717
714
|
return false;
|
|
718
715
|
}
|
|
@@ -758,9 +755,13 @@ export class AcpAgent implements Agent {
|
|
|
758
755
|
}
|
|
759
756
|
return true;
|
|
760
757
|
}
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
758
|
+
const slashText = text.trimStart();
|
|
759
|
+
if (!slashText.startsWith("/")) {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
const spaceIndex = slashText.indexOf(" ");
|
|
763
|
+
const commandName = spaceIndex === -1 ? slashText.slice(1) : slashText.slice(1, spaceIndex);
|
|
764
|
+
const args = spaceIndex === -1 ? "" : slashText.slice(spaceIndex + 1).trim();
|
|
764
765
|
if (
|
|
765
766
|
!isNamespacedSkillSlashCommandName(commandName) &&
|
|
766
767
|
options.directAliasMayCollide !== true &&
|
|
@@ -23,12 +23,11 @@ import {
|
|
|
23
23
|
extractPrintableText,
|
|
24
24
|
Input,
|
|
25
25
|
matchesKey,
|
|
26
|
-
padding,
|
|
27
26
|
replaceTabs,
|
|
27
|
+
resolveTerminalColumns,
|
|
28
28
|
Spacer,
|
|
29
29
|
Text,
|
|
30
30
|
truncateToWidth,
|
|
31
|
-
visibleWidth,
|
|
32
31
|
wrapTextWithAnsi,
|
|
33
32
|
} from "@sayknow-cli/tui";
|
|
34
33
|
import { isEnoent, prompt } from "@sayknow-cli/utils";
|
|
@@ -51,6 +50,7 @@ import { shortenPath } from "../../tools/render-utils";
|
|
|
51
50
|
import { theme } from "../theme/theme";
|
|
52
51
|
import { matchesAppInterrupt } from "../utils/keybinding-matchers";
|
|
53
52
|
import { DynamicBorder } from "./dynamic-border";
|
|
53
|
+
import { TwoColumnBody } from "./two-column-body";
|
|
54
54
|
|
|
55
55
|
type SourceTabId = "all" | AgentSource;
|
|
56
56
|
type AgentScope = "project" | "user";
|
|
@@ -295,38 +295,6 @@ class AgentInspectorPane implements Component {
|
|
|
295
295
|
invalidate(): void {}
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
class TwoColumnBody implements Component {
|
|
299
|
-
constructor(
|
|
300
|
-
private readonly leftPane: AgentListPane,
|
|
301
|
-
private readonly rightPane: AgentInspectorPane,
|
|
302
|
-
private readonly maxHeight: number,
|
|
303
|
-
) {}
|
|
304
|
-
|
|
305
|
-
render(width: number): string[] {
|
|
306
|
-
const leftWidth = Math.floor(width * 0.5);
|
|
307
|
-
const rightWidth = width - leftWidth - 3;
|
|
308
|
-
const leftLines = this.leftPane.render(leftWidth);
|
|
309
|
-
const rightLines = this.rightPane.render(rightWidth);
|
|
310
|
-
const lineCount = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
|
|
311
|
-
const out: string[] = [];
|
|
312
|
-
const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
|
313
|
-
|
|
314
|
-
for (let i = 0; i < lineCount; i++) {
|
|
315
|
-
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
|
316
|
-
const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
|
|
317
|
-
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
|
|
318
|
-
out.push(leftPadded + separator + right);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return out;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
invalidate(): void {
|
|
325
|
-
this.leftPane.invalidate?.();
|
|
326
|
-
this.rightPane.invalidate?.();
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
298
|
export class AgentDashboard extends Container {
|
|
331
299
|
#settingsManager: Settings | null = null;
|
|
332
300
|
#allAgents: DashboardAgent[] = [];
|
|
@@ -870,7 +838,7 @@ export class AgentDashboard extends Container {
|
|
|
870
838
|
}
|
|
871
839
|
|
|
872
840
|
#uiWidth(): number {
|
|
873
|
-
return Math.max(40,
|
|
841
|
+
return Math.max(40, resolveTerminalColumns());
|
|
874
842
|
}
|
|
875
843
|
|
|
876
844
|
/** Rebuild layout and request a TUI render pass (for use after async state changes). */
|
|
@@ -9,6 +9,7 @@ type ConfigurableEditorAction = Extract<
|
|
|
9
9
|
| "app.exit"
|
|
10
10
|
| "app.suspend"
|
|
11
11
|
| "app.thinking.cycle"
|
|
12
|
+
| "app.commandPalette.open"
|
|
12
13
|
| "app.model.cycleForward"
|
|
13
14
|
| "app.model.cycleBackward"
|
|
14
15
|
| "app.model.select"
|
|
@@ -33,6 +34,7 @@ const CONFIGURABLE_EDITOR_ACTIONS = [
|
|
|
33
34
|
"app.exit",
|
|
34
35
|
"app.suspend",
|
|
35
36
|
"app.thinking.cycle",
|
|
37
|
+
"app.commandPalette.open",
|
|
36
38
|
"app.model.cycleForward",
|
|
37
39
|
"app.model.cycleBackward",
|
|
38
40
|
"app.model.select",
|
|
@@ -76,6 +78,7 @@ export class CustomEditor extends Editor {
|
|
|
76
78
|
onClear?: () => void;
|
|
77
79
|
onExit?: () => void;
|
|
78
80
|
onCycleThinkingLevel?: () => void;
|
|
81
|
+
onOpenCommandPalette?: () => void;
|
|
79
82
|
onCycleModelForward?: () => void;
|
|
80
83
|
onCycleModelBackward?: () => void;
|
|
81
84
|
onSelectModel?: () => void;
|
|
@@ -100,6 +103,10 @@ export class CustomEditor extends Editor {
|
|
|
100
103
|
onQueue?: () => void;
|
|
101
104
|
/** Called when Caps Lock is pressed. */
|
|
102
105
|
onCapsLock?: () => void;
|
|
106
|
+
/** Called when PageUp/PageDown should scroll the transcript viewport instead of prompt history. */
|
|
107
|
+
onViewportPageScroll?: (direction: -1 | 1) => void;
|
|
108
|
+
/** Called before regular composer input should return the transcript viewport to the live bottom. */
|
|
109
|
+
onViewportFollowLive?: () => void;
|
|
103
110
|
|
|
104
111
|
/** Custom key handlers from extensions and non-built-in app actions. */
|
|
105
112
|
#customKeyHandlers = new Map<KeyId, () => boolean | undefined>();
|
|
@@ -240,6 +247,20 @@ export class CustomEditor extends Editor {
|
|
|
240
247
|
return;
|
|
241
248
|
}
|
|
242
249
|
}
|
|
250
|
+
if (!this.isShowingAutocomplete()) {
|
|
251
|
+
if (matchesKey(data, "pageUp") && this.onViewportPageScroll) {
|
|
252
|
+
this.onViewportPageScroll(-1);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (matchesKey(data, "pageDown") && this.onViewportPageScroll) {
|
|
256
|
+
this.onViewportPageScroll(1);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!matchesKey(data, "pageUp") && !matchesKey(data, "pageDown")) {
|
|
262
|
+
this.onViewportFollowLive?.();
|
|
263
|
+
}
|
|
243
264
|
// Intercept configured image paste (async - fires and handles result)
|
|
244
265
|
if (this.#matchesAction(data, "app.clipboard.pasteImage") && this.onPasteImage) {
|
|
245
266
|
void this.onPasteImage();
|
|
@@ -288,6 +309,11 @@ export class CustomEditor extends Editor {
|
|
|
288
309
|
return;
|
|
289
310
|
}
|
|
290
311
|
|
|
312
|
+
// Intercept configured command palette shortcut before model cycling (Ctrl+P default).
|
|
313
|
+
if (this.#matchesAction(data, "app.commandPalette.open") && this.onOpenCommandPalette) {
|
|
314
|
+
this.onOpenCommandPalette();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
291
317
|
// Intercept configured backward model cycling (check before forward cycling)
|
|
292
318
|
if (this.#matchesAction(data, "app.model.cycleBackward") && this.onCycleModelBackward) {
|
|
293
319
|
this.onCycleModelBackward();
|
|
@@ -6,15 +6,6 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
6
6
|
|
|
7
7
|
const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
8
8
|
|
|
9
|
-
type WizardStep = "name" | "display-name" | "provider" | "model" | "confirm";
|
|
10
|
-
|
|
11
|
-
interface WizardState {
|
|
12
|
-
name: string;
|
|
13
|
-
displayName: string;
|
|
14
|
-
provider: string;
|
|
15
|
-
model: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
9
|
export interface CustomModelPresetWizardSubmit {
|
|
19
10
|
name: string;
|
|
20
11
|
profile: ModelProfileConfig;
|
|
@@ -23,25 +14,21 @@ export interface CustomModelPresetWizardSubmit {
|
|
|
23
14
|
export class CustomModelPresetWizardComponent extends Container {
|
|
24
15
|
#contentContainer: Container;
|
|
25
16
|
#input: Input | null = null;
|
|
26
|
-
#step: WizardStep = "name";
|
|
27
|
-
#selectedIndex = 0;
|
|
28
17
|
#lastError: string | null = null;
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
displayName: "",
|
|
32
|
-
provider: "",
|
|
33
|
-
model: "",
|
|
34
|
-
};
|
|
18
|
+
#name = "";
|
|
19
|
+
#snapshot: ModelProfileConfig;
|
|
35
20
|
#onSubmit: (input: CustomModelPresetWizardSubmit) => void;
|
|
36
21
|
#onCancel: () => void;
|
|
37
22
|
#onRender: () => void;
|
|
38
23
|
|
|
39
24
|
constructor(
|
|
25
|
+
snapshot: ModelProfileConfig,
|
|
40
26
|
onSubmit: (input: CustomModelPresetWizardSubmit) => void,
|
|
41
27
|
onCancel: () => void,
|
|
42
28
|
onRender: () => void = () => {},
|
|
43
29
|
) {
|
|
44
30
|
super();
|
|
31
|
+
this.#snapshot = snapshot;
|
|
45
32
|
this.#onSubmit = onSubmit;
|
|
46
33
|
this.#onCancel = onCancel;
|
|
47
34
|
this.#onRender = onRender;
|
|
@@ -51,7 +38,7 @@ export class CustomModelPresetWizardComponent extends Container {
|
|
|
51
38
|
this.addChild(new TruncatedText(theme.bold("Create custom model preset")));
|
|
52
39
|
this.addChild(
|
|
53
40
|
new TruncatedText(
|
|
54
|
-
theme.fg("muted", " Save
|
|
41
|
+
theme.fg("muted", " Save the current default and explicit role models as a selectable profile."),
|
|
55
42
|
0,
|
|
56
43
|
0,
|
|
57
44
|
),
|
|
@@ -72,222 +59,76 @@ export class CustomModelPresetWizardComponent extends Container {
|
|
|
72
59
|
|
|
73
60
|
handleInput(keyData: string): void {
|
|
74
61
|
if (matchesAppInterrupt(keyData)) {
|
|
75
|
-
|
|
76
|
-
this.#onCancel();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
this.#goBack();
|
|
62
|
+
this.#onCancel();
|
|
80
63
|
return;
|
|
81
64
|
}
|
|
82
65
|
|
|
83
66
|
if (this.#input) {
|
|
84
67
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
85
|
-
this.#
|
|
68
|
+
this.#saveInputAndSubmit();
|
|
86
69
|
return;
|
|
87
70
|
}
|
|
88
71
|
this.#input.handleInput(keyData);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (matchesKey(keyData, "up")) {
|
|
93
|
-
this.#moveSelection(-1);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (matchesKey(keyData, "down")) {
|
|
97
|
-
this.#moveSelection(1);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
101
|
-
this.#selectCurrentOption();
|
|
102
72
|
}
|
|
103
73
|
}
|
|
104
74
|
|
|
105
75
|
#renderStep(): void {
|
|
106
76
|
this.#contentContainer.clear();
|
|
107
77
|
this.#input = null;
|
|
108
|
-
|
|
109
|
-
case "name":
|
|
110
|
-
this.#renderInputStep(
|
|
111
|
-
"Step 1: Preset id",
|
|
112
|
-
"Enter a unique preset id:",
|
|
113
|
-
this.#state.name,
|
|
114
|
-
"e.g. my-fast-coder",
|
|
115
|
-
);
|
|
116
|
-
break;
|
|
117
|
-
case "display-name":
|
|
118
|
-
this.#renderInputStep(
|
|
119
|
-
"Step 2: Display name",
|
|
120
|
-
"Enter a display name:",
|
|
121
|
-
this.#state.displayName,
|
|
122
|
-
"e.g. My Fast Coder",
|
|
123
|
-
);
|
|
124
|
-
break;
|
|
125
|
-
case "provider":
|
|
126
|
-
this.#renderInputStep(
|
|
127
|
-
"Step 3: Provider",
|
|
128
|
-
"Enter the provider id:",
|
|
129
|
-
this.#state.provider,
|
|
130
|
-
"e.g. openai-codex, anthropic, my-oai",
|
|
131
|
-
);
|
|
132
|
-
break;
|
|
133
|
-
case "model":
|
|
134
|
-
this.#renderInputStep(
|
|
135
|
-
"Step 4: Model",
|
|
136
|
-
"Enter the model id or provider/model selector:",
|
|
137
|
-
this.#state.model,
|
|
138
|
-
"e.g. gpt-5.5:medium or my-oai/gpt-example:low",
|
|
139
|
-
);
|
|
140
|
-
break;
|
|
141
|
-
case "confirm":
|
|
142
|
-
this.#renderConfirmStep();
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
#renderInputStep(title: string, prompt: string, value: string, hint: string): void {
|
|
148
|
-
this.#contentContainer.addChild(new Text(theme.fg("accent", title)));
|
|
78
|
+
this.#contentContainer.addChild(new Text(theme.fg("accent", "Preset id")));
|
|
149
79
|
this.#contentContainer.addChild(new Spacer(1));
|
|
150
80
|
if (this.#lastError) {
|
|
151
81
|
this.#contentContainer.addChild(new Text(theme.fg("error", this.#lastError), 0, 0));
|
|
152
82
|
this.#contentContainer.addChild(new Spacer(1));
|
|
153
83
|
}
|
|
154
|
-
this.#contentContainer.addChild(new Text(
|
|
84
|
+
this.#contentContainer.addChild(new Text("Enter a unique preset id:", 0, 0));
|
|
155
85
|
this.#contentContainer.addChild(new Spacer(1));
|
|
156
86
|
this.#input = new Input();
|
|
157
|
-
this.#input.setValue(
|
|
87
|
+
this.#input.setValue(this.#name);
|
|
158
88
|
this.#contentContainer.addChild(this.#input);
|
|
159
89
|
this.#contentContainer.addChild(new Spacer(1));
|
|
160
|
-
this.#
|
|
161
|
-
this.#addHelp("
|
|
90
|
+
this.#addSnapshotPreview();
|
|
91
|
+
this.#addHelp("e.g. my-fast-coder");
|
|
92
|
+
this.#addHelp("[Enter to create, Esc to cancel]");
|
|
162
93
|
}
|
|
163
94
|
|
|
164
|
-
#
|
|
165
|
-
this.#contentContainer.addChild(new Text(theme.fg("
|
|
166
|
-
this.#
|
|
167
|
-
|
|
168
|
-
this.#contentContainer.addChild(new Text(theme.fg("error", this.#lastError), 0, 0));
|
|
169
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
95
|
+
#addSnapshotPreview(): void {
|
|
96
|
+
this.#contentContainer.addChild(new Text(theme.fg("muted", "Snapshot:"), 0, 0));
|
|
97
|
+
for (const [role, selector] of Object.entries(this.#snapshot.model_mapping)) {
|
|
98
|
+
this.#contentContainer.addChild(new Text(` ${role}: ${selector}`, 0, 0));
|
|
170
99
|
}
|
|
171
|
-
this.#contentContainer.addChild(new Text(`
|
|
172
|
-
this.#contentContainer.addChild(new Text(`Display name: ${this.#state.displayName}`, 0, 0));
|
|
173
|
-
this.#contentContainer.addChild(new Text(`Provider: ${this.#state.provider}`, 0, 0));
|
|
174
|
-
this.#contentContainer.addChild(new Text(`Default model: ${this.#selector()}`, 0, 0));
|
|
100
|
+
this.#contentContainer.addChild(new Text(` providers: ${this.#snapshot.required_providers.join(", ")}`, 0, 0));
|
|
175
101
|
this.#contentContainer.addChild(new Spacer(1));
|
|
176
|
-
this.#addOption(0, "Create preset");
|
|
177
|
-
this.#addOption(1, "Go back");
|
|
178
|
-
this.#addHelp("[↑↓ to navigate, Enter to select, Esc to go back]");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
#addOption(index: number, label: string): void {
|
|
182
|
-
const selected = index === this.#selectedIndex;
|
|
183
|
-
const prefix = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
184
|
-
this.#contentContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
185
102
|
}
|
|
186
103
|
|
|
187
104
|
#addHelp(text: string): void {
|
|
188
105
|
this.#contentContainer.addChild(new Text(theme.fg("muted", text), 0, 0));
|
|
189
106
|
}
|
|
190
107
|
|
|
191
|
-
#
|
|
108
|
+
#saveInputAndSubmit(): void {
|
|
192
109
|
const value = this.#input?.getValue().trim() ?? "";
|
|
193
110
|
if (!value) {
|
|
194
|
-
this.#lastError =
|
|
111
|
+
this.#lastError = "Preset id is required.";
|
|
195
112
|
this.#renderStep();
|
|
196
113
|
this.#onRender();
|
|
197
114
|
return;
|
|
198
115
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
this.#lastError = validationError;
|
|
116
|
+
if (!PROFILE_NAME_PATTERN.test(value)) {
|
|
117
|
+
this.#lastError = "Preset id must use lowercase letters, numbers, dots, underscores, or hyphens.";
|
|
202
118
|
this.#renderStep();
|
|
203
119
|
this.#onRender();
|
|
204
120
|
return;
|
|
205
121
|
}
|
|
122
|
+
this.#name = value;
|
|
206
123
|
this.#lastError = null;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.#step = "display-name";
|
|
210
|
-
} else if (this.#step === "display-name") {
|
|
211
|
-
this.#state.displayName = value;
|
|
212
|
-
this.#step = "provider";
|
|
213
|
-
} else if (this.#step === "provider") {
|
|
214
|
-
this.#state.provider = value;
|
|
215
|
-
this.#step = "model";
|
|
216
|
-
} else if (this.#step === "model") {
|
|
217
|
-
this.#state.model = value;
|
|
218
|
-
this.#step = "confirm";
|
|
219
|
-
this.#selectedIndex = 0;
|
|
220
|
-
}
|
|
221
|
-
this.#renderStep();
|
|
222
|
-
this.#onRender();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
#emptyFieldMessage(): string {
|
|
226
|
-
switch (this.#step) {
|
|
227
|
-
case "name":
|
|
228
|
-
return "Preset id is required.";
|
|
229
|
-
case "display-name":
|
|
230
|
-
return "Display name is required.";
|
|
231
|
-
case "provider":
|
|
232
|
-
return "Provider is required.";
|
|
233
|
-
case "model":
|
|
234
|
-
return "Model is required.";
|
|
235
|
-
case "confirm":
|
|
236
|
-
return "Value is required.";
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
#validateCurrentInput(value: string): string | undefined {
|
|
241
|
-
if (this.#step === "name" && !PROFILE_NAME_PATTERN.test(value)) {
|
|
242
|
-
return "Preset id must use lowercase letters, numbers, dots, underscores, or hyphens.";
|
|
243
|
-
}
|
|
244
|
-
if (this.#step === "provider" && !PROFILE_NAME_PATTERN.test(value)) {
|
|
245
|
-
return "Provider id must use lowercase letters, numbers, dots, underscores, or hyphens.";
|
|
246
|
-
}
|
|
247
|
-
if (this.#step === "model" && value.includes(",")) {
|
|
248
|
-
return "Model must be one selector, not a comma-separated list.";
|
|
249
|
-
}
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
#selectCurrentOption(): void {
|
|
254
|
-
if (this.#step !== "confirm") return;
|
|
255
|
-
if (this.#selectedIndex === 0) {
|
|
256
|
-
this.#onSubmit(this.#buildInput());
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
this.#goBack();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
#buildInput(): CustomModelPresetWizardSubmit {
|
|
263
|
-
return {
|
|
264
|
-
name: this.#state.name,
|
|
124
|
+
this.#onSubmit({
|
|
125
|
+
name: value,
|
|
265
126
|
profile: {
|
|
266
|
-
|
|
267
|
-
display_name:
|
|
268
|
-
model_mapping: {
|
|
127
|
+
...this.#snapshot,
|
|
128
|
+
display_name: value,
|
|
129
|
+
model_mapping: { ...this.#snapshot.model_mapping },
|
|
130
|
+
required_providers: [...this.#snapshot.required_providers],
|
|
269
131
|
},
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
#selector(): string {
|
|
274
|
-
return this.#state.model.includes("/") ? this.#state.model : `${this.#state.provider}/${this.#state.model}`;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
#moveSelection(delta: number): void {
|
|
278
|
-
this.#selectedIndex = (this.#selectedIndex + delta + 2) % 2;
|
|
279
|
-
this.#renderStep();
|
|
280
|
-
this.#onRender();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
#goBack(): void {
|
|
284
|
-
if (this.#step === "display-name") this.#step = "name";
|
|
285
|
-
else if (this.#step === "provider") this.#step = "display-name";
|
|
286
|
-
else if (this.#step === "model") this.#step = "provider";
|
|
287
|
-
else if (this.#step === "confirm") this.#step = "model";
|
|
288
|
-
this.#selectedIndex = 0;
|
|
289
|
-
this.#lastError = null;
|
|
290
|
-
this.#renderStep();
|
|
291
|
-
this.#onRender();
|
|
132
|
+
});
|
|
292
133
|
}
|
|
293
134
|
}
|
|
@@ -11,20 +11,12 @@
|
|
|
11
11
|
* - Space: Toggle selected item (or master switch)
|
|
12
12
|
* - Esc: Close dashboard (clears search first if active)
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
15
|
-
type Component,
|
|
16
|
-
Container,
|
|
17
|
-
matchesKey,
|
|
18
|
-
padding,
|
|
19
|
-
Spacer,
|
|
20
|
-
Text,
|
|
21
|
-
truncateToWidth,
|
|
22
|
-
visibleWidth,
|
|
23
|
-
} from "@sayknow-cli/tui";
|
|
14
|
+
import { Container, matchesKey, Spacer, Text } from "@sayknow-cli/tui";
|
|
24
15
|
import { Settings } from "../../../config/settings";
|
|
25
16
|
import { DynamicBorder } from "../../../modes/components/dynamic-border";
|
|
26
17
|
import { theme } from "../../../modes/theme/theme";
|
|
27
18
|
import { matchesAppInterrupt } from "../../../modes/utils/keybinding-matchers";
|
|
19
|
+
import { TwoColumnBody } from "../two-column-body";
|
|
28
20
|
import { ExtensionList } from "./extension-list";
|
|
29
21
|
import { InspectorPanel } from "./inspector-panel";
|
|
30
22
|
import {
|
|
@@ -310,41 +302,3 @@ export class ExtensionDashboard extends Container {
|
|
|
310
302
|
}
|
|
311
303
|
}
|
|
312
304
|
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Two-column body component for side-by-side rendering.
|
|
316
|
-
*/
|
|
317
|
-
class TwoColumnBody implements Component {
|
|
318
|
-
constructor(
|
|
319
|
-
private readonly leftPane: ExtensionList,
|
|
320
|
-
private readonly rightPane: InspectorPanel,
|
|
321
|
-
private readonly maxHeight: number,
|
|
322
|
-
) {}
|
|
323
|
-
|
|
324
|
-
render(width: number): string[] {
|
|
325
|
-
const leftWidth = Math.floor(width * 0.5);
|
|
326
|
-
const rightWidth = Math.max(0, width - leftWidth - 3);
|
|
327
|
-
|
|
328
|
-
const leftLines = this.leftPane.render(leftWidth);
|
|
329
|
-
const rightLines = this.rightPane.render(rightWidth);
|
|
330
|
-
|
|
331
|
-
// Limit to maxHeight lines
|
|
332
|
-
const numLines = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
|
|
333
|
-
const combined: string[] = [];
|
|
334
|
-
const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
|
335
|
-
|
|
336
|
-
for (let i = 0; i < numLines; i++) {
|
|
337
|
-
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
|
338
|
-
const leftPadded = left + padding(Math.max(0, leftWidth - visibleWidth(left)));
|
|
339
|
-
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
|
|
340
|
-
combined.push(leftPadded + separator + right);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return combined;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
invalidate(): void {
|
|
347
|
-
this.leftPane.invalidate?.();
|
|
348
|
-
this.rightPane.invalidate?.();
|
|
349
|
-
}
|
|
350
|
-
}
|
|
@@ -59,6 +59,11 @@ export interface HookSelectorOptions {
|
|
|
59
59
|
optionLabel: string;
|
|
60
60
|
onSubmit: (text: string) => void;
|
|
61
61
|
};
|
|
62
|
+
clarificationInput?: {
|
|
63
|
+
optionLabel: string;
|
|
64
|
+
onSubmit: (text: string) => void;
|
|
65
|
+
allowEmpty?: boolean;
|
|
66
|
+
};
|
|
62
67
|
/**
|
|
63
68
|
* Autocomplete provider for the inline custom-input editor. When present,
|
|
64
69
|
* the "Other (type your own)" editor gains the same `@` file-link and `/`
|
|
@@ -339,6 +344,8 @@ export class HookSelectorComponent extends Container {
|
|
|
339
344
|
#outline: boolean;
|
|
340
345
|
#scrollTitleRows: number | undefined;
|
|
341
346
|
#customInput: { optionLabel: string; onSubmit: (text: string) => void } | undefined;
|
|
347
|
+
#clarificationInput: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
348
|
+
#activeInput: { onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
342
349
|
#inputArea: Container;
|
|
343
350
|
#inlineEditor: Editor | undefined;
|
|
344
351
|
#helpTextComponent: Text;
|
|
@@ -366,6 +373,7 @@ export class HookSelectorComponent extends Container {
|
|
|
366
373
|
this.#wrapFocused = opts?.wrapFocused === true;
|
|
367
374
|
this.#outline = opts?.outline === true;
|
|
368
375
|
this.#customInput = opts?.customInput;
|
|
376
|
+
this.#clarificationInput = opts?.clarificationInput;
|
|
369
377
|
this.#tui = opts?.tui;
|
|
370
378
|
this.#autocompleteProvider = opts?.autocompleteProvider;
|
|
371
379
|
|
|
@@ -501,7 +509,11 @@ export class HookSelectorComponent extends Container {
|
|
|
501
509
|
const selected = this.#options[this.#selectedIndex];
|
|
502
510
|
if (!selected) return;
|
|
503
511
|
if (this.#customInput && selected === this.#customInput.optionLabel) {
|
|
504
|
-
this.#enterInputMode();
|
|
512
|
+
this.#enterInputMode(this.#customInput);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (this.#clarificationInput && selected === this.#clarificationInput.optionLabel) {
|
|
516
|
+
this.#enterInputMode(this.#clarificationInput);
|
|
505
517
|
return;
|
|
506
518
|
}
|
|
507
519
|
this.#onSelectCallback(selected);
|
|
@@ -536,14 +548,19 @@ export class HookSelectorComponent extends Container {
|
|
|
536
548
|
return;
|
|
537
549
|
}
|
|
538
550
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
|
|
539
|
-
|
|
551
|
+
const text = editor.getExpandedText();
|
|
552
|
+
if (this.#activeInput?.allowEmpty === false && text.trim() === "") {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
this.#activeInput?.onSubmit(text);
|
|
540
556
|
return;
|
|
541
557
|
}
|
|
542
558
|
editor.handleInput(keyData);
|
|
543
559
|
}
|
|
544
560
|
|
|
545
|
-
#enterInputMode(): void {
|
|
561
|
+
#enterInputMode(input: { onSubmit: (text: string) => void; allowEmpty?: boolean }): void {
|
|
546
562
|
if (this.#inlineEditor) return;
|
|
563
|
+
this.#activeInput = input;
|
|
547
564
|
// Stop the auto-select countdown for good: the user is actively typing,
|
|
548
565
|
// matching the old behavior where the separate editor had no timeout.
|
|
549
566
|
if (this.#countdown) {
|
|
@@ -578,6 +595,7 @@ export class HookSelectorComponent extends Container {
|
|
|
578
595
|
#exitInputMode(): void {
|
|
579
596
|
if (!this.#inlineEditor) return;
|
|
580
597
|
this.#inlineEditor = undefined;
|
|
598
|
+
this.#activeInput = undefined;
|
|
581
599
|
this.#inputArea.clear();
|
|
582
600
|
this.#helpTextComponent.setText(theme.fg("dim", this.#baseHelpText));
|
|
583
601
|
this.invalidate();
|