longer-agent 0.1.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/LICENSE +21 -0
- package/README.md +227 -0
- package/README.zh-CN.md +227 -0
- package/agent_templates/executor/agent.yaml +22 -0
- package/agent_templates/executor/system_prompt.md +17 -0
- package/agent_templates/explorer/agent.yaml +13 -0
- package/agent_templates/explorer/system_prompt.md +19 -0
- package/agent_templates/main/agent.yaml +7 -0
- package/agent_templates/main/system_prompt.md +45 -0
- package/configExample.yaml +83 -0
- package/dist/agents/agent.d.ts +79 -0
- package/dist/agents/agent.d.ts.map +1 -0
- package/dist/agents/agent.js +156 -0
- package/dist/agents/agent.js.map +1 -0
- package/dist/agents/tool-loop.d.ts +140 -0
- package/dist/agents/tool-loop.d.ts.map +1 -0
- package/dist/agents/tool-loop.js +465 -0
- package/dist/agents/tool-loop.js.map +1 -0
- package/dist/ask.d.ts +81 -0
- package/dist/ask.d.ts.map +1 -0
- package/dist/ask.js +34 -0
- package/dist/ask.js.map +1 -0
- package/dist/auth/openai-oauth.d.ts +66 -0
- package/dist/auth/openai-oauth.d.ts.map +1 -0
- package/dist/auth/openai-oauth.js +640 -0
- package/dist/auth/openai-oauth.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +254 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +118 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +862 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +130 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +648 -0
- package/dist/config.js.map +1 -0
- package/dist/context-rendering.d.ts +69 -0
- package/dist/context-rendering.d.ts.map +1 -0
- package/dist/context-rendering.js +250 -0
- package/dist/context-rendering.js.map +1 -0
- package/dist/document-projection.d.ts +12 -0
- package/dist/document-projection.d.ts.map +1 -0
- package/dist/document-projection.js +75 -0
- package/dist/document-projection.js.map +1 -0
- package/dist/ephemeral-log.d.ts +15 -0
- package/dist/ephemeral-log.d.ts.map +1 -0
- package/dist/ephemeral-log.js +173 -0
- package/dist/ephemeral-log.js.map +1 -0
- package/dist/file-attach.d.ts +89 -0
- package/dist/file-attach.d.ts.map +1 -0
- package/dist/file-attach.js +571 -0
- package/dist/file-attach.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/init-wizard.d.ts +13 -0
- package/dist/init-wizard.d.ts.map +1 -0
- package/dist/init-wizard.js +328 -0
- package/dist/init-wizard.js.map +1 -0
- package/dist/log-entry.d.ts +104 -0
- package/dist/log-entry.d.ts.map +1 -0
- package/dist/log-entry.js +292 -0
- package/dist/log-entry.js.map +1 -0
- package/dist/log-projection.d.ts +73 -0
- package/dist/log-projection.d.ts.map +1 -0
- package/dist/log-projection.js +651 -0
- package/dist/log-projection.js.map +1 -0
- package/dist/mcp-client.d.ts +55 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +402 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-selection.d.ts +16 -0
- package/dist/model-selection.d.ts.map +1 -0
- package/dist/model-selection.js +181 -0
- package/dist/model-selection.js.map +1 -0
- package/dist/network-retry.d.ts +38 -0
- package/dist/network-retry.d.ts.map +1 -0
- package/dist/network-retry.js +140 -0
- package/dist/network-retry.js.map +1 -0
- package/dist/persistence.d.ts +104 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +644 -0
- package/dist/persistence.js.map +1 -0
- package/dist/primitives/context.d.ts +29 -0
- package/dist/primitives/context.d.ts.map +1 -0
- package/dist/primitives/context.js +85 -0
- package/dist/primitives/context.js.map +1 -0
- package/dist/progress.d.ts +51 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +229 -0
- package/dist/progress.js.map +1 -0
- package/dist/provider-presets.d.ts +34 -0
- package/dist/provider-presets.d.ts.map +1 -0
- package/dist/provider-presets.js +181 -0
- package/dist/provider-presets.js.map +1 -0
- package/dist/providers/anthropic.d.ts +32 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +450 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/base.d.ts +135 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +104 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/glm.d.ts +18 -0
- package/dist/providers/glm.d.ts.map +1 -0
- package/dist/providers/glm.js +59 -0
- package/dist/providers/glm.js.map +1 -0
- package/dist/providers/kimi.d.ts +23 -0
- package/dist/providers/kimi.d.ts.map +1 -0
- package/dist/providers/kimi.js +89 -0
- package/dist/providers/kimi.js.map +1 -0
- package/dist/providers/minimax.d.ts +20 -0
- package/dist/providers/minimax.d.ts.map +1 -0
- package/dist/providers/minimax.js +192 -0
- package/dist/providers/minimax.js.map +1 -0
- package/dist/providers/openai-chat.d.ts +33 -0
- package/dist/providers/openai-chat.d.ts.map +1 -0
- package/dist/providers/openai-chat.js +543 -0
- package/dist/providers/openai-chat.js.map +1 -0
- package/dist/providers/openai-responses.d.ts +26 -0
- package/dist/providers/openai-responses.d.ts.map +1 -0
- package/dist/providers/openai-responses.js +443 -0
- package/dist/providers/openai-responses.js.map +1 -0
- package/dist/providers/openrouter.d.ts +24 -0
- package/dist/providers/openrouter.d.ts.map +1 -0
- package/dist/providers/openrouter.js +177 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +38 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/security/path.d.ts +51 -0
- package/dist/security/path.d.ts.map +1 -0
- package/dist/security/path.js +187 -0
- package/dist/security/path.js.map +1 -0
- package/dist/security/sensitive-files.d.ts +3 -0
- package/dist/security/sensitive-files.d.ts.map +1 -0
- package/dist/security/sensitive-files.js +41 -0
- package/dist/security/sensitive-files.js.map +1 -0
- package/dist/session.d.ts +446 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +4595 -0
- package/dist/session.js.map +1 -0
- package/dist/settings.d.ts +46 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +134 -0
- package/dist/settings.js.map +1 -0
- package/dist/show-context.d.ts +35 -0
- package/dist/show-context.d.ts.map +1 -0
- package/dist/show-context.js +320 -0
- package/dist/show-context.js.map +1 -0
- package/dist/skills/loader.d.ts +49 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +166 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/summarize-context.d.ts +29 -0
- package/dist/summarize-context.d.ts.map +1 -0
- package/dist/summarize-context.js +247 -0
- package/dist/summarize-context.js.map +1 -0
- package/dist/templates/loader.d.ts +104 -0
- package/dist/templates/loader.d.ts.map +1 -0
- package/dist/templates/loader.js +514 -0
- package/dist/templates/loader.js.map +1 -0
- package/dist/tools/basic.d.ts +29 -0
- package/dist/tools/basic.d.ts.map +1 -0
- package/dist/tools/basic.js +2079 -0
- package/dist/tools/basic.js.map +1 -0
- package/dist/tools/comm.d.ts +17 -0
- package/dist/tools/comm.d.ts.map +1 -0
- package/dist/tools/comm.js +192 -0
- package/dist/tools/comm.js.map +1 -0
- package/dist/tools/web-fetch.d.ts +11 -0
- package/dist/tools/web-fetch.d.ts.map +1 -0
- package/dist/tools/web-fetch.js +237 -0
- package/dist/tools/web-fetch.js.map +1 -0
- package/dist/tools/web-search.d.ts +24 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +51 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/tui/app.d.ts +35 -0
- package/dist/tui/app.d.ts.map +1 -0
- package/dist/tui/app.js +1042 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/checkbox-picker.d.ts +35 -0
- package/dist/tui/checkbox-picker.d.ts.map +1 -0
- package/dist/tui/checkbox-picker.js +85 -0
- package/dist/tui/checkbox-picker.js.map +1 -0
- package/dist/tui/command-picker.d.ts +31 -0
- package/dist/tui/command-picker.d.ts.map +1 -0
- package/dist/tui/command-picker.js +113 -0
- package/dist/tui/command-picker.js.map +1 -0
- package/dist/tui/components/ask-panel.d.ts +21 -0
- package/dist/tui/components/ask-panel.d.ts.map +1 -0
- package/dist/tui/components/ask-panel.js +81 -0
- package/dist/tui/components/ask-panel.js.map +1 -0
- package/dist/tui/components/conversation-panel.d.ts +68 -0
- package/dist/tui/components/conversation-panel.d.ts.map +1 -0
- package/dist/tui/components/conversation-panel.js +611 -0
- package/dist/tui/components/conversation-panel.js.map +1 -0
- package/dist/tui/components/input-panel.d.ts +27 -0
- package/dist/tui/components/input-panel.d.ts.map +1 -0
- package/dist/tui/components/input-panel.js +725 -0
- package/dist/tui/components/input-panel.js.map +1 -0
- package/dist/tui/components/logo-panel.d.ts +14 -0
- package/dist/tui/components/logo-panel.d.ts.map +1 -0
- package/dist/tui/components/logo-panel.js +37 -0
- package/dist/tui/components/logo-panel.js.map +1 -0
- package/dist/tui/components/plan-panel.d.ts +10 -0
- package/dist/tui/components/plan-panel.d.ts.map +1 -0
- package/dist/tui/components/plan-panel.js +8 -0
- package/dist/tui/components/plan-panel.js.map +1 -0
- package/dist/tui/components/status-bar.d.ts +24 -0
- package/dist/tui/components/status-bar.d.ts.map +1 -0
- package/dist/tui/components/status-bar.js +80 -0
- package/dist/tui/components/status-bar.js.map +1 -0
- package/dist/tui/input/editor-state.d.ts +22 -0
- package/dist/tui/input/editor-state.d.ts.map +1 -0
- package/dist/tui/input/editor-state.js +157 -0
- package/dist/tui/input/editor-state.js.map +1 -0
- package/dist/tui/input/keymap.d.ts +3 -0
- package/dist/tui/input/keymap.d.ts.map +1 -0
- package/dist/tui/input/keymap.js +72 -0
- package/dist/tui/input/keymap.js.map +1 -0
- package/dist/tui/input/paste-slots.d.ts +17 -0
- package/dist/tui/input/paste-slots.d.ts.map +1 -0
- package/dist/tui/input/paste-slots.js +46 -0
- package/dist/tui/input/paste-slots.js.map +1 -0
- package/dist/tui/input/paste.d.ts +15 -0
- package/dist/tui/input/paste.d.ts.map +1 -0
- package/dist/tui/input/paste.js +35 -0
- package/dist/tui/input/paste.js.map +1 -0
- package/dist/tui/input/protocol.d.ts +9 -0
- package/dist/tui/input/protocol.d.ts.map +1 -0
- package/dist/tui/input/protocol.js +387 -0
- package/dist/tui/input/protocol.js.map +1 -0
- package/dist/tui/input/sanitize.d.ts +6 -0
- package/dist/tui/input/sanitize.d.ts.map +1 -0
- package/dist/tui/input/sanitize.js +20 -0
- package/dist/tui/input/sanitize.js.map +1 -0
- package/dist/tui/input/types.d.ts +18 -0
- package/dist/tui/input/types.d.ts.map +1 -0
- package/dist/tui/input/types.js +2 -0
- package/dist/tui/input/types.js.map +1 -0
- package/dist/tui/launch.d.ts +23 -0
- package/dist/tui/launch.d.ts.map +1 -0
- package/dist/tui/launch.js +104 -0
- package/dist/tui/launch.js.map +1 -0
- package/dist/tui/theme.d.ts +20 -0
- package/dist/tui/theme.d.ts.map +1 -0
- package/dist/tui/theme.js +29 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/tui/types.d.ts +136 -0
- package/dist/tui/types.d.ts.map +1 -0
- package/dist/tui/types.js +9 -0
- package/dist/tui/types.js.map +1 -0
- package/package.json +76 -0
- package/prompts/sections/agents_md.md +23 -0
- package/prompts/sections/important_log.md +16 -0
- package/prompts/sections/system_mechanisms.md +18 -0
- package/prompts/tools/apply_patch.md +31 -0
- package/prompts/tools/ask.md +18 -0
- package/prompts/tools/bash.md +13 -0
- package/prompts/tools/bash_background.md +9 -0
- package/prompts/tools/bash_output.md +9 -0
- package/prompts/tools/check_status.md +3 -0
- package/prompts/tools/diff.md +5 -0
- package/prompts/tools/edit_file.md +11 -0
- package/prompts/tools/glob.md +7 -0
- package/prompts/tools/grep.md +20 -0
- package/prompts/tools/kill_agent.md +3 -0
- package/prompts/tools/kill_shell.md +5 -0
- package/prompts/tools/list_dir.md +5 -0
- package/prompts/tools/plan.md +252 -0
- package/prompts/tools/read_file.md +9 -0
- package/prompts/tools/show_context.md +12 -0
- package/prompts/tools/skill.md +7 -0
- package/prompts/tools/spawn_agent.md +195 -0
- package/prompts/tools/summarize_context.md +122 -0
- package/prompts/tools/test.md +5 -0
- package/prompts/tools/wait.md +17 -0
- package/prompts/tools/web_fetch.md +9 -0
- package/prompts/tools/web_search.md +5 -0
- package/prompts/tools/write_file.md +11 -0
- package/skills/.staging/.gitkeep +0 -0
- package/skills/explain-code/SKILL.md +15 -0
- package/skills/skill-manager/SKILL.md +83 -0
package/dist/commands.js
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extensible slash-command system.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
*
|
|
6
|
+
* const registry = buildDefaultRegistry();
|
|
7
|
+
* const cmd = registry.lookup("/help");
|
|
8
|
+
* if (cmd) {
|
|
9
|
+
* await cmd.handler(ctx, "");
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { loadLog, validateAndRepairLog } from "./persistence.js";
|
|
15
|
+
import { formatDisplayModelName, formatScopedModelName, getThinkingLevels, } from "./config.js";
|
|
16
|
+
import { PROVIDER_PRESETS, } from "./provider-presets.js";
|
|
17
|
+
import { resolveModelSelection as resolveModelSelectionCore } from "./model-selection.js";
|
|
18
|
+
import { resolveSkillContent } from "./skills/loader.js";
|
|
19
|
+
import { ACCENT_PRESETS, setAccent, theme } from "./tui/theme.js";
|
|
20
|
+
import { hasOAuthTokens } from "./auth/openai-oauth.js";
|
|
21
|
+
export class CommandExitSignal extends Error {
|
|
22
|
+
code;
|
|
23
|
+
constructor(code = 0) {
|
|
24
|
+
super(`Command requested exit (${code})`);
|
|
25
|
+
this.name = "CommandExitSignal";
|
|
26
|
+
this.code = code;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function isCommandExitSignal(err) {
|
|
30
|
+
return err instanceof CommandExitSignal ||
|
|
31
|
+
(err?.name === "CommandExitSignal" &&
|
|
32
|
+
typeof err?.code === "number");
|
|
33
|
+
}
|
|
34
|
+
// ------------------------------------------------------------------
|
|
35
|
+
// CommandRegistry
|
|
36
|
+
// ------------------------------------------------------------------
|
|
37
|
+
export class CommandRegistry {
|
|
38
|
+
_commands = new Map();
|
|
39
|
+
/** Register a command. Overwrites any existing command with the same name. */
|
|
40
|
+
register(cmd) {
|
|
41
|
+
this._commands.set(cmd.name, cmd);
|
|
42
|
+
}
|
|
43
|
+
/** Remove a command by its exact name. Returns true if it existed. */
|
|
44
|
+
unregister(name) {
|
|
45
|
+
return this._commands.delete(name);
|
|
46
|
+
}
|
|
47
|
+
/** Look up a command by its exact name. */
|
|
48
|
+
lookup(name) {
|
|
49
|
+
return this._commands.get(name);
|
|
50
|
+
}
|
|
51
|
+
/** Return all registered commands sorted alphabetically by name. */
|
|
52
|
+
getAll() {
|
|
53
|
+
return Array.from(this._commands.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
54
|
+
}
|
|
55
|
+
/** Return command names that start with the given prefix (for completion). */
|
|
56
|
+
getCompletions(prefix) {
|
|
57
|
+
const results = [];
|
|
58
|
+
for (const name of Array.from(this._commands.keys())) {
|
|
59
|
+
if (name.startsWith(prefix)) {
|
|
60
|
+
results.push(name);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return results.sort();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ------------------------------------------------------------------
|
|
67
|
+
// Built-in command handlers
|
|
68
|
+
// ------------------------------------------------------------------
|
|
69
|
+
async function cmdHelp(ctx, _args) {
|
|
70
|
+
const lines = ["Commands:"];
|
|
71
|
+
for (const cmd of ctx.commandRegistry.getAll()) {
|
|
72
|
+
lines.push(` ${cmd.name} ${cmd.description}`);
|
|
73
|
+
}
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("Shortcuts:");
|
|
76
|
+
lines.push(" Enter Send message");
|
|
77
|
+
lines.push(" Option+Enter Insert newline");
|
|
78
|
+
lines.push(" Ctrl+N Insert newline");
|
|
79
|
+
lines.push(" Ctrl+G Toggle markdown raw view");
|
|
80
|
+
lines.push(" Cmd+Delete Delete to line start (Ghostty/kitty protocol)");
|
|
81
|
+
lines.push(" Alt+Backspace/Ctrl+W Delete previous word");
|
|
82
|
+
lines.push(" Ctrl+C Cancel / Exit");
|
|
83
|
+
lines.push(" @filename Attach file");
|
|
84
|
+
ctx.showMessage(lines.join("\n"));
|
|
85
|
+
}
|
|
86
|
+
async function cmdNew(ctx, _args) {
|
|
87
|
+
ctx.resetUiState();
|
|
88
|
+
ctx.autoSave();
|
|
89
|
+
// Clear session dir — a new directory will be created lazily on first save.
|
|
90
|
+
// This avoids creating an empty session file when the user doesn't send any messages.
|
|
91
|
+
if (ctx.store) {
|
|
92
|
+
ctx.store.clearSession();
|
|
93
|
+
}
|
|
94
|
+
// Full session reset — store is updated, then conversation re-initialized
|
|
95
|
+
// with correct paths. Equivalent to constructing a fresh Session.
|
|
96
|
+
ctx.session.resetForNewSession(ctx.store);
|
|
97
|
+
ctx.showMessage("--- New session started ---");
|
|
98
|
+
}
|
|
99
|
+
async function cmdSummarize(ctx, args) {
|
|
100
|
+
if (!ctx.onManualSummarizeRequested) {
|
|
101
|
+
ctx.showMessage("Manual summarize is not available in this UI.");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
ctx.onManualSummarizeRequested(args.trim());
|
|
105
|
+
}
|
|
106
|
+
async function cmdCompact(ctx, args) {
|
|
107
|
+
if (!ctx.onManualCompactRequested) {
|
|
108
|
+
ctx.showMessage("Manual compact is not available in this UI.");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
ctx.onManualCompactRequested(args.trim());
|
|
112
|
+
}
|
|
113
|
+
async function cmdResume(ctx, args) {
|
|
114
|
+
const store = ctx.store;
|
|
115
|
+
if (!store) {
|
|
116
|
+
ctx.showMessage("Session persistence not available.");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const sessions = store.listSessions();
|
|
120
|
+
if (sessions.length === 0) {
|
|
121
|
+
ctx.showMessage("No saved sessions found.");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const trimmed = args.trim();
|
|
125
|
+
if (!trimmed) {
|
|
126
|
+
// List sessions
|
|
127
|
+
const lines = ["Recent Sessions:"];
|
|
128
|
+
const shown = sessions.slice(0, 10);
|
|
129
|
+
for (let i = 0; i < shown.length; i++) {
|
|
130
|
+
const s = shown[i];
|
|
131
|
+
const created = s.created
|
|
132
|
+
? s.created.slice(0, 19).replace("T", " ")
|
|
133
|
+
: "?";
|
|
134
|
+
const summary = truncateDisplayText(s.summary || "(empty)", 25);
|
|
135
|
+
lines.push(` ${i + 1} ${created} ${s.turns}t ${summary}`);
|
|
136
|
+
}
|
|
137
|
+
lines.push("");
|
|
138
|
+
lines.push("Use /resume <number> to load a session.");
|
|
139
|
+
ctx.showMessage(lines.join("\n"));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Load specific session
|
|
143
|
+
const idx = parseInt(trimmed, 10) - 1;
|
|
144
|
+
if (isNaN(idx)) {
|
|
145
|
+
ctx.showMessage(`Invalid session number: ${trimmed}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (idx < 0 || idx >= sessions.length) {
|
|
149
|
+
ctx.showMessage(`Session number out of range (1-${sessions.length}).`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Auto-save current first
|
|
153
|
+
ctx.autoSave();
|
|
154
|
+
const target = sessions[idx];
|
|
155
|
+
const session = ctx.session;
|
|
156
|
+
const logJsonPath = join(target.path, "log.json");
|
|
157
|
+
const hasLogJson = existsSync(logJsonPath);
|
|
158
|
+
if (!hasLogJson) {
|
|
159
|
+
ctx.showMessage("No log.json found for this session.");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
let logData;
|
|
163
|
+
try {
|
|
164
|
+
logData = loadLog(target.path);
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
ctx.showMessage(`Failed to load log: ${e instanceof Error ? e.message : String(e)}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Validate and repair
|
|
171
|
+
const { entries: repairedEntries, repaired, warnings } = validateAndRepairLog(logData.entries);
|
|
172
|
+
if (repaired) {
|
|
173
|
+
for (const w of warnings) {
|
|
174
|
+
ctx.showMessage(`[repair] ${w}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
ctx.resetUiState();
|
|
178
|
+
try {
|
|
179
|
+
session.restoreFromLog(logData.meta, repairedEntries, logData.idAllocator);
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
ctx.showMessage(`Failed to restore session: ${e instanceof Error ? e.message : String(e)}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// Point store at the loaded session
|
|
186
|
+
store.sessionDir = target.path;
|
|
187
|
+
if (typeof session.setStore === "function") {
|
|
188
|
+
session.setStore(store);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function buildResumeOptionLabel(index, created, turns, summary) {
|
|
192
|
+
const date = (created || "").slice(0, 16);
|
|
193
|
+
return `${index + 1}. ${date} ${turns ?? 0} turns ${truncateDisplayText(summary || "", 25)}`;
|
|
194
|
+
}
|
|
195
|
+
function truncateDisplayText(text, maxChars) {
|
|
196
|
+
return Array.from(text).slice(0, maxChars).join("");
|
|
197
|
+
}
|
|
198
|
+
function resumeOptions(ctx) {
|
|
199
|
+
const store = ctx.store;
|
|
200
|
+
if (!store)
|
|
201
|
+
return [];
|
|
202
|
+
const sessions = store.listSessions();
|
|
203
|
+
return sessions.map((s, i) => ({
|
|
204
|
+
label: buildResumeOptionLabel(i, s.created, s.turns, s.summary),
|
|
205
|
+
value: String(i + 1),
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
async function cmdQuit(ctx, _args) {
|
|
209
|
+
if (ctx.exit) {
|
|
210
|
+
await ctx.exit();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
ctx.autoSave();
|
|
214
|
+
try {
|
|
215
|
+
if (typeof ctx.session.close === "function") {
|
|
216
|
+
await ctx.session.close();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// ignore
|
|
221
|
+
}
|
|
222
|
+
// Non-TUI callers decide how to handle shutdown.
|
|
223
|
+
throw new CommandExitSignal(0);
|
|
224
|
+
}
|
|
225
|
+
function currentSessionModelDisplayName(session) {
|
|
226
|
+
return formatDisplayModelName(session.primaryAgent?.modelConfig?.provider, session.currentModelName ?? session.primaryAgent?.modelConfig?.model);
|
|
227
|
+
}
|
|
228
|
+
function persistGlobalPreferences(ctx) {
|
|
229
|
+
if (!ctx.store || typeof ctx.store.saveGlobalPreferences !== "function")
|
|
230
|
+
return;
|
|
231
|
+
if (typeof ctx.session.getGlobalPreferences !== "function")
|
|
232
|
+
return;
|
|
233
|
+
try {
|
|
234
|
+
ctx.store.saveGlobalPreferences(ctx.session.getGlobalPreferences());
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Ignore preference persistence failures during command execution.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function thinkingOptions(ctx) {
|
|
241
|
+
const session = ctx.session;
|
|
242
|
+
const model = session.currentModelName ?? "";
|
|
243
|
+
const levels = getThinkingLevels(model);
|
|
244
|
+
const current = session.thinkingLevel ?? "default";
|
|
245
|
+
const opts = [];
|
|
246
|
+
// "default" is always available as reset option
|
|
247
|
+
opts.push({
|
|
248
|
+
label: current === "default" ? "default (current)" : "default",
|
|
249
|
+
value: "default",
|
|
250
|
+
});
|
|
251
|
+
for (const level of levels) {
|
|
252
|
+
const isCurrent = current === level;
|
|
253
|
+
opts.push({
|
|
254
|
+
label: isCurrent ? `${level} (current)` : level,
|
|
255
|
+
value: level,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return opts;
|
|
259
|
+
}
|
|
260
|
+
async function cmdThinking(ctx, args) {
|
|
261
|
+
const session = ctx.session;
|
|
262
|
+
const model = session.currentModelName;
|
|
263
|
+
const displayModel = currentSessionModelDisplayName(session);
|
|
264
|
+
const levels = getThinkingLevels(model);
|
|
265
|
+
const trimmed = args.trim().toLowerCase();
|
|
266
|
+
if (!trimmed) {
|
|
267
|
+
// No arg: show info (fallback for non-overlay usage)
|
|
268
|
+
const current = session.thinkingLevel;
|
|
269
|
+
if (!levels.length) {
|
|
270
|
+
ctx.showMessage(`Model '${displayModel}' does not support configurable thinking levels.`);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
ctx.showMessage(`Thinking level: ${current}\n` +
|
|
274
|
+
`Available levels for ${displayModel}: ${levels.join(", ")}`);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (trimmed === "default") {
|
|
279
|
+
session.thinkingLevel = "default";
|
|
280
|
+
persistGlobalPreferences(ctx);
|
|
281
|
+
ctx.showMessage("Thinking level reset to provider default.");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (levels.length && !levels.includes(trimmed)) {
|
|
285
|
+
ctx.showMessage(`Invalid level '${trimmed}' for ${displayModel}.\n` +
|
|
286
|
+
`Available: ${levels.join(", ")}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
session.thinkingLevel = trimmed;
|
|
290
|
+
persistGlobalPreferences(ctx);
|
|
291
|
+
ctx.showMessage(`Thinking level set to: ${trimmed}`);
|
|
292
|
+
}
|
|
293
|
+
function cacheHitOptions(ctx) {
|
|
294
|
+
const session = ctx.session;
|
|
295
|
+
const enabled = session.cacheHitEnabled ?? true;
|
|
296
|
+
return [
|
|
297
|
+
{ label: enabled ? "ON (current)" : "ON", value: "on" },
|
|
298
|
+
{ label: enabled ? "OFF" : "OFF (current)", value: "off" },
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
async function cmdCacheHit(ctx, args) {
|
|
302
|
+
const session = ctx.session;
|
|
303
|
+
const trimmed = args.trim().toLowerCase();
|
|
304
|
+
if (trimmed === "on") {
|
|
305
|
+
session.cacheHitEnabled = true;
|
|
306
|
+
}
|
|
307
|
+
else if (trimmed === "off") {
|
|
308
|
+
session.cacheHitEnabled = false;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// No argument toggles the current setting.
|
|
312
|
+
session.cacheHitEnabled = !session.cacheHitEnabled;
|
|
313
|
+
}
|
|
314
|
+
persistGlobalPreferences(ctx);
|
|
315
|
+
const state = session.cacheHitEnabled ? "ON" : "OFF";
|
|
316
|
+
const provider = session.primaryAgent?.modelConfig?.provider ?? "";
|
|
317
|
+
let note = "";
|
|
318
|
+
if (provider === "anthropic") {
|
|
319
|
+
note = session.cacheHitEnabled
|
|
320
|
+
? " (cache_control markers will be sent)"
|
|
321
|
+
: " (cache_control markers disabled)";
|
|
322
|
+
}
|
|
323
|
+
else if (provider === "openrouter") {
|
|
324
|
+
note = " (Cache is automatic via OpenRouter for supported models)";
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
note = " (Cache is automatic for this provider)";
|
|
328
|
+
}
|
|
329
|
+
ctx.showMessage(`Prompt caching: ${state}${note}`);
|
|
330
|
+
}
|
|
331
|
+
const PROVIDER_KEY_GROUP_ALIASES = {
|
|
332
|
+
"openai-chat": "openai",
|
|
333
|
+
"openai-responses": "openai",
|
|
334
|
+
"openai-codex": "openai-codex", // Separate group — uses OAuth, not shared API key
|
|
335
|
+
"kimi-cn": "kimi",
|
|
336
|
+
"kimi-ai": "kimi",
|
|
337
|
+
"kimi-code": "kimi",
|
|
338
|
+
"glm-intl": "glm",
|
|
339
|
+
"glm-code": "glm",
|
|
340
|
+
"glm-intl-code": "glm",
|
|
341
|
+
"minimax-cn": "minimax",
|
|
342
|
+
};
|
|
343
|
+
function providerKeyGroup(provider) {
|
|
344
|
+
return PROVIDER_KEY_GROUP_ALIASES[provider] ?? provider;
|
|
345
|
+
}
|
|
346
|
+
const PROVIDER_ENV_VARS = (() => {
|
|
347
|
+
const map = new Map();
|
|
348
|
+
for (const p of PROVIDER_PRESETS) {
|
|
349
|
+
const group = providerKeyGroup(p.id);
|
|
350
|
+
if (!map.has(group))
|
|
351
|
+
map.set(group, p.envVar);
|
|
352
|
+
}
|
|
353
|
+
return map;
|
|
354
|
+
})();
|
|
355
|
+
function readModelEntries(config) {
|
|
356
|
+
if (typeof config?.listModelEntries === "function") {
|
|
357
|
+
try {
|
|
358
|
+
const entries = config.listModelEntries();
|
|
359
|
+
if (Array.isArray(entries))
|
|
360
|
+
return entries;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// Fall through to compatibility mode.
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Compatibility for old/partial config stubs (best-effort only).
|
|
367
|
+
const out = [];
|
|
368
|
+
for (const name of config?.modelNames ?? []) {
|
|
369
|
+
try {
|
|
370
|
+
const mc = config.getModel(name);
|
|
371
|
+
out.push({
|
|
372
|
+
name,
|
|
373
|
+
provider: String(mc.provider ?? ""),
|
|
374
|
+
model: String(mc.model ?? ""),
|
|
375
|
+
apiKeyRaw: String(mc.apiKey ?? ""),
|
|
376
|
+
hasResolvedApiKey: Boolean(mc.apiKey),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// Ignore invalid entries.
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
function parseModelArgs(args) {
|
|
386
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
387
|
+
const target = tokens[0] ?? "";
|
|
388
|
+
const rest = tokens.slice(1);
|
|
389
|
+
let apiKey;
|
|
390
|
+
for (const t of rest) {
|
|
391
|
+
if (t.startsWith("key=")) {
|
|
392
|
+
apiKey = t.slice("key=".length);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
if (t.startsWith("api_key=")) {
|
|
396
|
+
apiKey = t.slice("api_key=".length);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!apiKey && rest.length === 1) {
|
|
401
|
+
apiKey = rest[0];
|
|
402
|
+
}
|
|
403
|
+
return { target, apiKey };
|
|
404
|
+
}
|
|
405
|
+
function parseProviderModelTarget(target) {
|
|
406
|
+
const idx = target.indexOf(":");
|
|
407
|
+
if (idx <= 0 || idx >= target.length - 1)
|
|
408
|
+
return null;
|
|
409
|
+
return {
|
|
410
|
+
provider: target.slice(0, idx),
|
|
411
|
+
model: target.slice(idx + 1),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function hasEnvApiKey(envVar) {
|
|
415
|
+
if (!envVar)
|
|
416
|
+
return false;
|
|
417
|
+
const raw = process.env[envVar];
|
|
418
|
+
return typeof raw === "string" && raw.trim() !== "";
|
|
419
|
+
}
|
|
420
|
+
function getProviderKeySource(entries, provider) {
|
|
421
|
+
const group = providerKeyGroup(provider);
|
|
422
|
+
const fromConfig = entries.find((e) => providerKeyGroup(e.provider) === group && e.hasResolvedApiKey && e.apiKeyRaw.trim() !== "");
|
|
423
|
+
if (fromConfig)
|
|
424
|
+
return fromConfig.apiKeyRaw;
|
|
425
|
+
const envVar = PROVIDER_ENV_VARS.get(group);
|
|
426
|
+
if (hasEnvApiKey(envVar))
|
|
427
|
+
return `\${${envVar}}`;
|
|
428
|
+
// OAuth fallback for openai-codex
|
|
429
|
+
if (provider === "openai-codex") {
|
|
430
|
+
try {
|
|
431
|
+
if (hasOAuthTokens())
|
|
432
|
+
return "oauth:openai-codex";
|
|
433
|
+
}
|
|
434
|
+
catch { /* ignore */ }
|
|
435
|
+
}
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
function runtimeModelName(provider, model) {
|
|
439
|
+
const slug = (s) => s
|
|
440
|
+
.toLowerCase()
|
|
441
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
442
|
+
.replace(/^-+|-+$/g, "");
|
|
443
|
+
return `runtime-${slug(provider)}-${slug(model)}`;
|
|
444
|
+
}
|
|
445
|
+
function formatPresetPickerLabel(provider, presetModel) {
|
|
446
|
+
let label = formatDisplayModelName(provider, presetModel.id);
|
|
447
|
+
if (presetModel.optionNote) {
|
|
448
|
+
label = `${label} (${presetModel.optionNote})`;
|
|
449
|
+
}
|
|
450
|
+
return label;
|
|
451
|
+
}
|
|
452
|
+
function formatPresetSelectedHint(provider, presetModel) {
|
|
453
|
+
let label = formatScopedModelName(provider, presetModel.id);
|
|
454
|
+
if (presetModel.optionNote) {
|
|
455
|
+
label = `${label} (${presetModel.optionNote})`;
|
|
456
|
+
}
|
|
457
|
+
return label;
|
|
458
|
+
}
|
|
459
|
+
export function resolveModelSelection(session, target, apiKey) {
|
|
460
|
+
return resolveModelSelectionCore(session, target, apiKey);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Build model children (leaf-level options) for a single provider.
|
|
464
|
+
*/
|
|
465
|
+
function buildModelChildren(provider, byProvider, providerHasKey, session, currentProvider, currentModel) {
|
|
466
|
+
const models = Array.from((byProvider.get(provider) ?? new Map()).entries());
|
|
467
|
+
models.sort((a, b) => a[1].label.localeCompare(b[1].label));
|
|
468
|
+
const children = [];
|
|
469
|
+
for (const [selectionKey, item] of models) {
|
|
470
|
+
const runtimeSelectionName = runtimeModelName(provider, selectionKey);
|
|
471
|
+
const isCurrent = session.currentModelConfigName === runtimeSelectionName
|
|
472
|
+
|| (selectionKey === item.model
|
|
473
|
+
&& provider === currentProvider
|
|
474
|
+
&& item.model === currentModel);
|
|
475
|
+
const missingApiKey = !providerHasKey.get(providerKeyGroup(provider));
|
|
476
|
+
const missingHint = provider === "openai-codex"
|
|
477
|
+
? "not logged in: run longeragent oauth"
|
|
478
|
+
: "key missing: run longeragent init";
|
|
479
|
+
let label = item.label;
|
|
480
|
+
if (isCurrent && missingApiKey) {
|
|
481
|
+
label = `${label} (current, ${missingHint})`;
|
|
482
|
+
}
|
|
483
|
+
else if (isCurrent) {
|
|
484
|
+
label = `${label} (current)`;
|
|
485
|
+
}
|
|
486
|
+
else if (missingApiKey) {
|
|
487
|
+
label = `${label} (${missingHint})`;
|
|
488
|
+
}
|
|
489
|
+
children.push({
|
|
490
|
+
label,
|
|
491
|
+
value: `${provider}:${selectionKey}`,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return children;
|
|
495
|
+
}
|
|
496
|
+
/** Display names for OpenRouter vendor prefixes. */
|
|
497
|
+
const OPENROUTER_VENDOR_NAMES = {
|
|
498
|
+
"anthropic": "Anthropic",
|
|
499
|
+
"openai": "OpenAI",
|
|
500
|
+
"moonshotai": "Kimi",
|
|
501
|
+
"minimax": "MiniMax",
|
|
502
|
+
"z-ai": "GLM / Zhipu",
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* Build options for /model picker.
|
|
506
|
+
*
|
|
507
|
+
* Supports three structures:
|
|
508
|
+
* - Two-level: provider → model (for ungrouped providers like anthropic, openai)
|
|
509
|
+
* - Three-level via group field: group → sub-provider → model (kimi, glm, minimax)
|
|
510
|
+
* - Three-level via vendor prefix: openrouter → vendor → model
|
|
511
|
+
*/
|
|
512
|
+
function modelOptions(ctx) {
|
|
513
|
+
const session = ctx.session;
|
|
514
|
+
const config = session.config;
|
|
515
|
+
if (!config)
|
|
516
|
+
return [];
|
|
517
|
+
const entries = readModelEntries(config);
|
|
518
|
+
const currentProvider = String(session.primaryAgent?.modelConfig?.provider ?? "");
|
|
519
|
+
const currentModel = String(session.primaryAgent?.modelConfig?.model ?? "");
|
|
520
|
+
// Gather all providers/models:
|
|
521
|
+
// 1) preset catalog
|
|
522
|
+
// 2) user-defined config models (for custom IDs/providers)
|
|
523
|
+
const byProvider = new Map();
|
|
524
|
+
const providerOrder = [];
|
|
525
|
+
const addModel = (provider, selectionKey, model, label) => {
|
|
526
|
+
if (!provider || !selectionKey || !model)
|
|
527
|
+
return;
|
|
528
|
+
if (!byProvider.has(provider)) {
|
|
529
|
+
byProvider.set(provider, new Map());
|
|
530
|
+
providerOrder.push(provider);
|
|
531
|
+
}
|
|
532
|
+
if (!byProvider.get(provider).has(selectionKey)) {
|
|
533
|
+
byProvider.get(provider).set(selectionKey, { model, label });
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
for (const preset of PROVIDER_PRESETS) {
|
|
537
|
+
for (const m of preset.models) {
|
|
538
|
+
addModel(preset.id, m.key, m.id, formatPresetPickerLabel(preset.id, m));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
for (const e of entries) {
|
|
542
|
+
addModel(e.provider, e.model, e.model, formatDisplayModelName(e.provider, e.model));
|
|
543
|
+
}
|
|
544
|
+
// Provider-level key status from config/env/current model.
|
|
545
|
+
const providerHasKey = new Map();
|
|
546
|
+
for (const e of entries) {
|
|
547
|
+
if (e.hasResolvedApiKey) {
|
|
548
|
+
providerHasKey.set(providerKeyGroup(e.provider), true);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
for (const [group, envVar] of PROVIDER_ENV_VARS) {
|
|
552
|
+
if (hasEnvApiKey(envVar))
|
|
553
|
+
providerHasKey.set(group, true);
|
|
554
|
+
}
|
|
555
|
+
// OAuth: check token store for openai-codex (sync, no HTTP)
|
|
556
|
+
try {
|
|
557
|
+
if (hasOAuthTokens())
|
|
558
|
+
providerHasKey.set("openai-codex", true);
|
|
559
|
+
}
|
|
560
|
+
catch { /* auth module not available */ }
|
|
561
|
+
const currentProviderGroup = providerKeyGroup(currentProvider);
|
|
562
|
+
if (session.primaryAgent?.modelConfig?.apiKey) {
|
|
563
|
+
providerHasKey.set(currentProviderGroup, true);
|
|
564
|
+
}
|
|
565
|
+
// Build a lookup from provider id → preset (for group metadata).
|
|
566
|
+
const presetById = new Map();
|
|
567
|
+
for (const p of PROVIDER_PRESETS) {
|
|
568
|
+
presetById.set(p.id, p);
|
|
569
|
+
}
|
|
570
|
+
const options = [];
|
|
571
|
+
const processed = new Set();
|
|
572
|
+
for (const provider of providerOrder) {
|
|
573
|
+
if (processed.has(provider))
|
|
574
|
+
continue;
|
|
575
|
+
processed.add(provider);
|
|
576
|
+
const preset = presetById.get(provider);
|
|
577
|
+
// ── Three-level: grouped providers (kimi, glm, minimax) ──
|
|
578
|
+
if (preset?.group) {
|
|
579
|
+
// Collect all providers in this group (preserving providerOrder).
|
|
580
|
+
const groupMembers = providerOrder.filter((p) => {
|
|
581
|
+
const pp = presetById.get(p);
|
|
582
|
+
return pp?.group === preset.group;
|
|
583
|
+
});
|
|
584
|
+
for (const gp of groupMembers)
|
|
585
|
+
processed.add(gp);
|
|
586
|
+
const subOptions = [];
|
|
587
|
+
let groupHasCurrent = false;
|
|
588
|
+
for (const gp of groupMembers) {
|
|
589
|
+
const gpPreset = presetById.get(gp);
|
|
590
|
+
const children = buildModelChildren(gp, byProvider, providerHasKey, session, currentProvider, currentModel);
|
|
591
|
+
const subHasCurrent = children.some((c) => c.label.includes("(current)"));
|
|
592
|
+
if (subHasCurrent)
|
|
593
|
+
groupHasCurrent = true;
|
|
594
|
+
const subLabel = gpPreset?.subLabel ?? gp;
|
|
595
|
+
subOptions.push({
|
|
596
|
+
label: subHasCurrent ? `${subLabel} (current)` : subLabel,
|
|
597
|
+
value: gp,
|
|
598
|
+
children,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
const groupLabel = preset.groupLabel ?? preset.group;
|
|
602
|
+
options.push({
|
|
603
|
+
label: groupHasCurrent ? `${groupLabel} (current)` : groupLabel,
|
|
604
|
+
value: preset.group,
|
|
605
|
+
children: subOptions,
|
|
606
|
+
});
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
// ── Three-level: OpenRouter (sub-group by vendor prefix) ──
|
|
610
|
+
if (provider === "openrouter") {
|
|
611
|
+
const children = buildModelChildren(provider, byProvider, providerHasKey, session, currentProvider, currentModel);
|
|
612
|
+
// Group children by vendor prefix (e.g. "anthropic/..." → "anthropic").
|
|
613
|
+
const vendorGroups = new Map();
|
|
614
|
+
const vendorOrder = [];
|
|
615
|
+
for (const child of children) {
|
|
616
|
+
const modelKey = child.value.split(":")[1] ?? "";
|
|
617
|
+
const slashIdx = modelKey.indexOf("/");
|
|
618
|
+
const vendor = slashIdx > 0 ? modelKey.slice(0, slashIdx) : "other";
|
|
619
|
+
if (!vendorGroups.has(vendor)) {
|
|
620
|
+
vendorGroups.set(vendor, []);
|
|
621
|
+
vendorOrder.push(vendor);
|
|
622
|
+
}
|
|
623
|
+
vendorGroups.get(vendor).push(child);
|
|
624
|
+
}
|
|
625
|
+
const subOptions = [];
|
|
626
|
+
let openrouterHasCurrent = false;
|
|
627
|
+
for (const vendor of vendorOrder) {
|
|
628
|
+
const vendorChildren = vendorGroups.get(vendor);
|
|
629
|
+
const vendorHasCurrent = vendorChildren.some((c) => c.label.includes("(current)"));
|
|
630
|
+
if (vendorHasCurrent)
|
|
631
|
+
openrouterHasCurrent = true;
|
|
632
|
+
const displayName = OPENROUTER_VENDOR_NAMES[vendor] ?? vendor;
|
|
633
|
+
subOptions.push({
|
|
634
|
+
label: vendorHasCurrent ? `${displayName} (current)` : displayName,
|
|
635
|
+
value: `openrouter-${vendor}`,
|
|
636
|
+
children: vendorChildren,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
options.push({
|
|
640
|
+
label: openrouterHasCurrent ? "openrouter (current)" : "openrouter",
|
|
641
|
+
value: "openrouter",
|
|
642
|
+
children: subOptions,
|
|
643
|
+
});
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
// ── Two-level: ungrouped providers (anthropic, openai, user-defined) ──
|
|
647
|
+
const children = buildModelChildren(provider, byProvider, providerHasKey, session, currentProvider, currentModel);
|
|
648
|
+
const hasCurrent = children.some((c) => c.label.includes("(current)"));
|
|
649
|
+
options.push({
|
|
650
|
+
label: hasCurrent ? `${provider} (current)` : provider,
|
|
651
|
+
value: provider,
|
|
652
|
+
children,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return options;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* /model command: switch model by creating a new session.
|
|
659
|
+
*
|
|
660
|
+
* The selected value is either a config name or a provider:model target.
|
|
661
|
+
*/
|
|
662
|
+
async function cmdModel(ctx, args) {
|
|
663
|
+
const session = ctx.session;
|
|
664
|
+
const trimmed = args.trim();
|
|
665
|
+
if (!trimmed) {
|
|
666
|
+
const displayCurrent = currentSessionModelDisplayName(session) || "unknown";
|
|
667
|
+
const current = session.currentModelConfigName
|
|
668
|
+
? `${session.currentModelConfigName} (${displayCurrent})`
|
|
669
|
+
: displayCurrent;
|
|
670
|
+
ctx.showMessage(`Current model: ${current}\n` +
|
|
671
|
+
"Use /model to select a new model.\n" +
|
|
672
|
+
"For models marked 'key missing', run 'longeragent init' (or use /model provider:model key=YOUR_API_KEY).");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (!session.switchModel) {
|
|
676
|
+
ctx.showMessage("Model switching is not supported in this session.");
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const { target, apiKey } = parseModelArgs(trimmed);
|
|
681
|
+
const resolvedSelection = resolveModelSelection(session, target, apiKey);
|
|
682
|
+
const { selectedConfigName, selectedHint } = resolvedSelection;
|
|
683
|
+
// Save current session before switching
|
|
684
|
+
ctx.resetUiState();
|
|
685
|
+
ctx.autoSave();
|
|
686
|
+
if (ctx.store) {
|
|
687
|
+
ctx.store.clearSession();
|
|
688
|
+
}
|
|
689
|
+
// Switch model, then create fresh session
|
|
690
|
+
session.switchModel(selectedConfigName);
|
|
691
|
+
session.setPersistedModelSelection?.({
|
|
692
|
+
modelConfigName: selectedConfigName,
|
|
693
|
+
modelProvider: resolvedSelection.modelProvider,
|
|
694
|
+
modelSelectionKey: resolvedSelection.modelSelectionKey,
|
|
695
|
+
modelId: resolvedSelection.modelId,
|
|
696
|
+
});
|
|
697
|
+
session.resetForNewSession(ctx.store);
|
|
698
|
+
persistGlobalPreferences(ctx);
|
|
699
|
+
const mc = session.primaryAgent?.modelConfig;
|
|
700
|
+
if (mc) {
|
|
701
|
+
ctx.showMessage(`--- New session with ${selectedHint} (${formatScopedModelName(mc.provider, mc.model)}) ---\n` +
|
|
702
|
+
` Context: ${(mc.contextLength ?? 0).toLocaleString()} tokens`);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
ctx.showMessage(`--- New session with ${selectedHint} ---`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
catch (e) {
|
|
709
|
+
ctx.showMessage(`Failed to switch model: ${e instanceof Error ? e.message : String(e)}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// ------------------------------------------------------------------
|
|
713
|
+
// /theme command
|
|
714
|
+
// ------------------------------------------------------------------
|
|
715
|
+
function themeOptions(_ctx) {
|
|
716
|
+
const current = theme.accent;
|
|
717
|
+
return ACCENT_PRESETS.map((preset) => {
|
|
718
|
+
const isCurrent = preset.value === current;
|
|
719
|
+
return {
|
|
720
|
+
label: isCurrent ? `${preset.label} (current)` : preset.label,
|
|
721
|
+
value: preset.value,
|
|
722
|
+
};
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
async function cmdTheme(ctx, args) {
|
|
726
|
+
const trimmed = args.trim();
|
|
727
|
+
if (!trimmed) {
|
|
728
|
+
ctx.showMessage(`Current accent: ${theme.accent}\n` +
|
|
729
|
+
"Use /theme to select a new accent color.");
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
// Accept preset label (case-insensitive) or raw hex value
|
|
733
|
+
const preset = ACCENT_PRESETS.find((p) => p.value === trimmed || p.label.toLowerCase() === trimmed.toLowerCase());
|
|
734
|
+
const color = preset ? preset.value : trimmed;
|
|
735
|
+
// Basic hex validation
|
|
736
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(color)) {
|
|
737
|
+
ctx.showMessage(`Invalid color: "${trimmed}". Use a preset name or a hex color like #3b82f6.`);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
setAccent(color);
|
|
741
|
+
ctx.session.accentColor = color;
|
|
742
|
+
persistGlobalPreferences(ctx);
|
|
743
|
+
const label = preset ? `${preset.label} (${color})` : color;
|
|
744
|
+
ctx.showMessage(`Accent color set to: ${label}`);
|
|
745
|
+
}
|
|
746
|
+
// ------------------------------------------------------------------
|
|
747
|
+
// Registry builder
|
|
748
|
+
// ------------------------------------------------------------------
|
|
749
|
+
/**
|
|
750
|
+
* Build the default command registry with all built-in commands.
|
|
751
|
+
*/
|
|
752
|
+
export function buildDefaultRegistry() {
|
|
753
|
+
const registry = new CommandRegistry();
|
|
754
|
+
registry.register({ name: "/help", description: "Show commands and shortcuts", handler: cmdHelp });
|
|
755
|
+
registry.register({ name: "/compact", description: "Manually compact the active context", handler: cmdCompact });
|
|
756
|
+
registry.register({ name: "/new", description: "Start a new session", handler: cmdNew });
|
|
757
|
+
registry.register({ name: "/resume", description: "Resume a previous session", handler: cmdResume, options: resumeOptions });
|
|
758
|
+
registry.register({ name: "/summarize", description: "Manually summarize older context", handler: cmdSummarize });
|
|
759
|
+
registry.register({ name: "/model", description: "Switch model", handler: cmdModel, options: modelOptions });
|
|
760
|
+
registry.register({ name: "/quit", description: "Exit the application", handler: cmdQuit });
|
|
761
|
+
registry.register({ name: "/exit", description: "Exit the application", handler: cmdQuit });
|
|
762
|
+
registry.register({ name: "/thinking", description: "Set thinking level", handler: cmdThinking, options: thinkingOptions });
|
|
763
|
+
registry.register({ name: "/cachehit", description: "Prompt caching", handler: cmdCacheHit, options: cacheHitOptions });
|
|
764
|
+
registry.register({ name: "/theme", description: "Change accent color", handler: cmdTheme, options: themeOptions });
|
|
765
|
+
registry.register({ name: "/skills", description: "Manage installed skills", handler: cmdSkills, options: skillsOptions, checkboxMode: true });
|
|
766
|
+
return registry;
|
|
767
|
+
}
|
|
768
|
+
// ------------------------------------------------------------------
|
|
769
|
+
// /skills command
|
|
770
|
+
// ------------------------------------------------------------------
|
|
771
|
+
function skillsOptions(ctx) {
|
|
772
|
+
const session = ctx.session;
|
|
773
|
+
if (!session?.getAllSkillNames)
|
|
774
|
+
return [];
|
|
775
|
+
const allSkills = session.getAllSkillNames();
|
|
776
|
+
if (allSkills.length === 0)
|
|
777
|
+
return [];
|
|
778
|
+
return allSkills.map((s) => ({
|
|
779
|
+
label: `${s.name} ${s.description.length > 50 ? s.description.slice(0, 47) + "..." : s.description}`,
|
|
780
|
+
value: s.name,
|
|
781
|
+
checked: s.enabled,
|
|
782
|
+
}));
|
|
783
|
+
}
|
|
784
|
+
async function cmdSkills(ctx, args) {
|
|
785
|
+
const session = ctx.session;
|
|
786
|
+
if (!session?.getAllSkillNames) {
|
|
787
|
+
ctx.showMessage("Skills system not available.");
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const trimmed = args.trim();
|
|
791
|
+
if (!trimmed) {
|
|
792
|
+
// No args — show list
|
|
793
|
+
const allSkills = session.getAllSkillNames();
|
|
794
|
+
if (allSkills.length === 0) {
|
|
795
|
+
ctx.showMessage("No skills installed.");
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const lines = ["Installed skills:"];
|
|
799
|
+
for (const s of allSkills) {
|
|
800
|
+
lines.push(` ${s.enabled ? "[x]" : "[ ]"} ${s.name} — ${s.description}`);
|
|
801
|
+
}
|
|
802
|
+
ctx.showMessage(lines.join("\n"));
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// Checkbox picker submits comma-separated enabled skill names
|
|
806
|
+
// Parse: all items were submitted, enabled ones are in the args
|
|
807
|
+
const enabledNames = new Set(trimmed.split(",").map((s) => s.trim()).filter(Boolean));
|
|
808
|
+
const allSkills = session.getAllSkillNames();
|
|
809
|
+
const oldSkills = session.skills;
|
|
810
|
+
for (const s of allSkills) {
|
|
811
|
+
session.setSkillEnabled(s.name, enabledNames.has(s.name));
|
|
812
|
+
}
|
|
813
|
+
session.reloadSkills();
|
|
814
|
+
// Re-register slash commands
|
|
815
|
+
reRegisterSkillCommands(ctx.commandRegistry, oldSkills, session.skills);
|
|
816
|
+
const enabledCount = enabledNames.size;
|
|
817
|
+
const totalCount = allSkills.length;
|
|
818
|
+
ctx.showMessage(`Skills updated: ${enabledCount}/${totalCount} enabled.`);
|
|
819
|
+
persistGlobalPreferences(ctx);
|
|
820
|
+
}
|
|
821
|
+
// ------------------------------------------------------------------
|
|
822
|
+
// Skill command registration
|
|
823
|
+
// ------------------------------------------------------------------
|
|
824
|
+
/**
|
|
825
|
+
* Register slash commands for user-invocable skills.
|
|
826
|
+
*
|
|
827
|
+
* Each skill with `userInvocable === true` gets a `/skill-name` command.
|
|
828
|
+
* When invoked, the skill content is injected and a turn is triggered.
|
|
829
|
+
*/
|
|
830
|
+
export function registerSkillCommands(registry, skills) {
|
|
831
|
+
for (const skill of skills.values()) {
|
|
832
|
+
if (!skill.userInvocable)
|
|
833
|
+
continue;
|
|
834
|
+
const captured = skill; // capture for closure
|
|
835
|
+
const desc = captured.description.length > 60
|
|
836
|
+
? captured.description.slice(0, 57) + "..."
|
|
837
|
+
: captured.description;
|
|
838
|
+
registry.register({
|
|
839
|
+
name: "/" + captured.name,
|
|
840
|
+
description: desc,
|
|
841
|
+
handler: async (ctx, args) => {
|
|
842
|
+
const content = resolveSkillContent(captured, args);
|
|
843
|
+
const tagged = `[SKILL: ${captured.name}]\n\n${content}`;
|
|
844
|
+
ctx.showMessage(`Loaded skill: ${captured.name}`);
|
|
845
|
+
if (ctx.onTurnRequested) {
|
|
846
|
+
ctx.onTurnRequested(tagged);
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Unregister old skill commands, then register new ones.
|
|
854
|
+
* Used after reloadSkills() to keep slash commands in sync.
|
|
855
|
+
*/
|
|
856
|
+
export function reRegisterSkillCommands(registry, oldSkills, newSkills) {
|
|
857
|
+
for (const skill of oldSkills.values()) {
|
|
858
|
+
registry.unregister("/" + skill.name);
|
|
859
|
+
}
|
|
860
|
+
registerSkillCommands(registry, newSkills);
|
|
861
|
+
}
|
|
862
|
+
//# sourceMappingURL=commands.js.map
|