@dreb/coding-agent 2.20.0 → 2.21.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/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +7 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/forbidden-commands.d.ts.map +1 -1
- package/dist/core/forbidden-commands.js +63 -2
- package/dist/core/forbidden-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +18 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +3 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +7 -5
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +4 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/tab-title.d.ts.map +1 -1
- package/dist/modes/interactive/tab-title.js +18 -11
- package/dist/modes/interactive/tab-title.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-title.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/tab-title.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"tab-title.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/tab-title.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,GAAG,EAAW,KAAK,EAAE,MAAM,UAAU,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAWvE,MAAM,WAAW,YAAY;IAC5B,0CAA0C;IAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,sDAAsD;IACtD,WAAW,EAAE,MAAM,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC9D,yEAAuE;IACvE,QAAQ,EAAE,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACvC,iDAAiD;IACjD,gBAAgB,EAAE,MAAM,aAAa,CAAC;IACtC,oCAAoC;IACpC,WAAW,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,qBAAa,iBAAiB;IAM5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANtB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,YACkB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,IAAI,EAAE,YAAY,EAGnC;IAED,yCAAyC;IACzC,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;OAGG;IACH,SAAS,IAAI,IAAI,CAShB;IAED,2DAAyD;IACzD,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,oEAAkE;IAClE,IAAI,QAAQ,IAAI,OAAO,CAEtB;YAEa,aAAa;YA8Bb,YAAY;IA4B1B,OAAO,CAAC,qBAAqB;IAqB7B,OAAO,CAAC,YAAY;IAyBpB,mDAAmD;IACnD,OAAO,CAAC,aAAa;CAsBrB","sourcesContent":["/**\n * Auto-generates a terminal tab title from session context after a threshold\n * number of tool calls. Uses a lightweight single-shot LLM call to produce a\n * concise ≤30 character title, then sets it via the terminal's OSC 0 escape.\n *\n * Fires at most once per session. Failures are swallowed silently.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { Api, Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { CONFIG_DIR_NAME, getPackageDir } from \"../../config.js\";\nimport type { ModelRegistry } from \"../../core/model-registry.js\";\nimport type { TabTitleSettings } from \"../../core/settings-manager.js\";\nimport { parseAgentFrontmatter, resolveModelForSubagentSpawn } from \"../../core/tools/subagent.js\";\n\nconst DEFAULT_TRIGGER_AFTER = 3;\nconst MAX_TITLE_LENGTH = 30;\nconst TITLE_GENERATION_TIMEOUT_MS = 60_000;\n\nconst TITLE_PROMPT =\n\t\"Summarize this session's task in ≤30 characters for a terminal tab title. \" +\n\t\"Output ONLY the title text, nothing else. No quotes, no explanation.\";\n\nexport interface TabTitleDeps {\n\t/** Set the terminal tab title (OSC 0). */\n\tsetTitle: (title: string) => void;\n\t/** Get the current session messages (for context). */\n\tgetMessages: () => Array<{ role: string; content?: unknown }>;\n\t/** Get the current model (parent session model — used as fallback). */\n\tgetModel: () => Model<Api> | undefined;\n\t/** Get model registry for API key resolution. */\n\tgetModelRegistry: () => ModelRegistry;\n\t/** Get the parent provider name. */\n\tgetProvider: () => string | undefined;\n}\n\nexport class TabTitleGenerator {\n\tprivate toolCallCount = 0;\n\tprivate fired = false;\n\tprivate readonly threshold: number;\n\n\tconstructor(\n\t\tprivate readonly settings: TabTitleSettings | undefined,\n\t\tprivate readonly deps: TabTitleDeps,\n\t) {\n\t\tthis.threshold = settings?.triggerAfter ?? DEFAULT_TRIGGER_AFTER;\n\t}\n\n\t/** Whether this generator is enabled. */\n\tget enabled(): boolean {\n\t\treturn this.settings?.enabled !== false;\n\t}\n\n\t/**\n\t * Called on each tool_execution_end event. Increments the counter and fires\n\t * the title generation when threshold is reached.\n\t */\n\tonToolEnd(): void {\n\t\tif (this.fired || !this.enabled) return;\n\n\t\tthis.toolCallCount++;\n\t\tif (this.toolCallCount >= this.threshold) {\n\t\t\tthis.fired = true;\n\t\t\t// Fire-and-forget — never surfaces errors to the user\n\t\t\tthis.generateTitle().catch(() => {});\n\t\t}\n\t}\n\n\t/** Exposed for testing — the current tool call count. */\n\tget currentCount(): number {\n\t\treturn this.toolCallCount;\n\t}\n\n\t/** Exposed for testing — whether the title has been generated. */\n\tget hasFired(): boolean {\n\t\treturn this.fired;\n\t}\n\n\tprivate async generateTitle(): Promise<void> {\n\t\t// Single timeout bounds the entire pipeline (model probing + API key + LLM call)\n\t\tconst signal = AbortSignal.timeout(TITLE_GENERATION_TIMEOUT_MS);\n\n\t\tconst model = await this.resolveModel(signal);\n\t\tif (!model) return;\n\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst apiKey = await registry.getApiKey(model);\n\n\t\tconst userContext = this.buildContext();\n\t\tif (!userContext) return;\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: TITLE_PROMPT,\n\t\t\tmessages: [{ role: \"user\", content: userContext, timestamp: Date.now() }],\n\t\t};\n\n\t\tconst response = await completeSimple(model, context, {\n\t\t\tapiKey,\n\t\t\tmaxRetryDelayMs: 0,\n\t\t\tsignal,\n\t\t});\n\n\t\tconst title = this.sanitizeTitle(response);\n\t\tif (title) {\n\t\t\tthis.deps.setTitle(`dreb - ${title}`);\n\t\t}\n\t}\n\n\tprivate async resolveModel(signal?: AbortSignal): Promise<Model<Api> | undefined> {\n\t\t// Try to get the Explore agent's model fallback list\n\t\tconst exploreModels = this.getExploreAgentModels();\n\t\tconst parentModel = this.deps.getModel();\n\t\tconst parentProvider = this.deps.getProvider();\n\t\tconst registry = this.deps.getModelRegistry();\n\n\t\tif (exploreModels) {\n\t\t\tconst resolution = await resolveModelForSubagentSpawn(\n\t\t\t\texploreModels,\n\t\t\t\tparentProvider,\n\t\t\t\tregistry,\n\t\t\t\tparentModel?.id,\n\t\t\t\tsignal,\n\t\t\t\t\"[tab-title]\",\n\t\t\t);\n\t\t\tif (resolution.ok) {\n\t\t\t\t// Find the resolved model in registry\n\t\t\t\tconst available = registry.getAvailable();\n\t\t\t\tconst found = available.find((m) => m.id === resolution.modelId);\n\t\t\t\tif (found) return found;\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to parent session model\n\t\treturn parentModel;\n\t}\n\n\tprivate getExploreAgentModels(): string | string[] | undefined {\n\t\t// Resolution order mirrors discoverAgentTypes: user override > project > package.\n\t\t// First match with a valid model wins.\n\t\tconst candidates = [\n\t\t\tjoin(homedir(), CONFIG_DIR_NAME, \"agents\", \"explore.md\"),\n\t\t\tjoin(process.cwd(), \".dreb\", \"agents\", \"explore.md\"),\n\t\t\tjoin(getPackageDir(), \"agents\", \"explore.md\"),\n\t\t];\n\n\t\tfor (const agentFile of candidates) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(agentFile, \"utf-8\");\n\t\t\t\tconst parsed = parseAgentFrontmatter(content);\n\t\t\t\tif (parsed.ok && parsed.config.model) {\n\t\t\t\t\treturn parsed.config.model;\n\t\t\t\t}\n\t\t\t} catch {}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate buildContext(): string | undefined {\n\t\tconst messages = this.deps.getMessages();\n\t\tif (messages.length === 0) return undefined;\n\n\t\t// Extract the first user message for context\n\t\tconst firstUser = messages.find((m) => m.role === \"user\");\n\t\tif (!firstUser) return undefined;\n\n\t\tconst content =\n\t\t\ttypeof firstUser.content === \"string\"\n\t\t\t\t? firstUser.content\n\t\t\t\t: Array.isArray(firstUser.content)\n\t\t\t\t\t? firstUser.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t: \"\";\n\n\t\tif (!content) return undefined;\n\n\t\t// Truncate long messages — the LLM only needs a summary\n\t\tconst truncated = content.length > 500 ? `${content.slice(0, 500)}...` : content;\n\t\treturn `Session task:\\n${truncated}`;\n\t}\n\n\t/** Clean up LLM response to a usable tab title. */\n\tprivate sanitizeTitle(response: unknown): string | undefined {\n\t\tif (!response || typeof response !== \"object\") return undefined;\n\n\t\tconst msg = response as { content?: Array<{ type: string; text?: string }> };\n\t\tif (!msg.content || !Array.isArray(msg.content)) return undefined;\n\n\t\tconst textPart = msg.content.find((c) => c.type === \"text\");\n\t\tif (!textPart?.text) return undefined;\n\n\t\tlet title = textPart.text.trim();\n\t\t// Strip surrounding quotes if present\n\t\tif ((title.startsWith('\"') && title.endsWith('\"')) || (title.startsWith(\"'\") && title.endsWith(\"'\"))) {\n\t\t\ttitle = title.slice(1, -1).trim();\n\t\t}\n\t\t// Remove newlines\n\t\ttitle = title.replace(/[\\r\\n]+/g, \" \").trim();\n\t\t// Truncate to max length\n\t\tif (title.length > MAX_TITLE_LENGTH) {\n\t\t\ttitle = title.slice(0, MAX_TITLE_LENGTH);\n\t\t}\n\t\treturn title || undefined;\n\t}\n}\n"]}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* Fires at most once per session. Failures are swallowed silently.
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
import { completeSimple } from "@dreb/ai";
|
|
11
|
-
import { getPackageDir } from "../../config.js";
|
|
12
|
+
import { CONFIG_DIR_NAME, getPackageDir } from "../../config.js";
|
|
12
13
|
import { parseAgentFrontmatter, resolveModelForSubagentSpawn } from "../../core/tools/subagent.js";
|
|
13
14
|
const DEFAULT_TRIGGER_AFTER = 3;
|
|
14
15
|
const MAX_TITLE_LENGTH = 30;
|
|
@@ -84,7 +85,7 @@ export class TabTitleGenerator {
|
|
|
84
85
|
const parentProvider = this.deps.getProvider();
|
|
85
86
|
const registry = this.deps.getModelRegistry();
|
|
86
87
|
if (exploreModels) {
|
|
87
|
-
const resolution = await resolveModelForSubagentSpawn(exploreModels, parentProvider, registry, parentModel?.id, signal);
|
|
88
|
+
const resolution = await resolveModelForSubagentSpawn(exploreModels, parentProvider, registry, parentModel?.id, signal, "[tab-title]");
|
|
88
89
|
if (resolution.ok) {
|
|
89
90
|
// Find the resolved model in registry
|
|
90
91
|
const available = registry.getAvailable();
|
|
@@ -97,16 +98,22 @@ export class TabTitleGenerator {
|
|
|
97
98
|
return parentModel;
|
|
98
99
|
}
|
|
99
100
|
getExploreAgentModels() {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
// Resolution order mirrors discoverAgentTypes: user override > project > package.
|
|
102
|
+
// First match with a valid model wins.
|
|
103
|
+
const candidates = [
|
|
104
|
+
join(homedir(), CONFIG_DIR_NAME, "agents", "explore.md"),
|
|
105
|
+
join(process.cwd(), ".dreb", "agents", "explore.md"),
|
|
106
|
+
join(getPackageDir(), "agents", "explore.md"),
|
|
107
|
+
];
|
|
108
|
+
for (const agentFile of candidates) {
|
|
109
|
+
try {
|
|
110
|
+
const content = readFileSync(agentFile, "utf-8");
|
|
111
|
+
const parsed = parseAgentFrontmatter(content);
|
|
112
|
+
if (parsed.ok && parsed.config.model) {
|
|
113
|
+
return parsed.config.model;
|
|
114
|
+
}
|
|
106
115
|
}
|
|
107
|
-
|
|
108
|
-
catch {
|
|
109
|
-
// Fall through to parent model
|
|
116
|
+
catch { }
|
|
110
117
|
}
|
|
111
118
|
return undefined;
|
|
112
119
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-title.js","sourceRoot":"","sources":["../../../src/modes/interactive/tab-title.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,8BAA8B,CAAC;AAEnG,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAChC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAC5B,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAE3C,MAAM,YAAY,GACjB,8EAA4E;IAC5E,sEAAsE,CAAC;AAexE,MAAM,OAAO,iBAAiB;IAMX,QAAQ;IACR,IAAI;IANd,aAAa,GAAG,CAAC,CAAC;IAClB,KAAK,GAAG,KAAK,CAAC;IACL,SAAS,CAAS;IAEnC,YACkB,QAAsC,EACtC,IAAkB,EAClC;wBAFgB,QAAQ;oBACR,IAAI;QAErB,IAAI,CAAC,SAAS,GAAG,QAAQ,EAAE,YAAY,IAAI,qBAAqB,CAAC;IAAA,CACjE;IAED,yCAAyC;IACzC,IAAI,OAAO,GAAY;QACtB,OAAO,IAAI,CAAC,QAAQ,EAAE,OAAO,KAAK,KAAK,CAAC;IAAA,CACxC;IAED;;;OAGG;IACH,SAAS,GAAS;QACjB,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAExC,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC1C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,wDAAsD;YACtD,IAAI,CAAC,aAAa,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;QACtC,CAAC;IAAA,CACD;IAED,2DAAyD;IACzD,IAAI,YAAY,GAAW;QAC1B,OAAO,IAAI,CAAC,aAAa,CAAC;IAAA,CAC1B;IAED,oEAAkE;IAClE,IAAI,QAAQ,GAAY;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC;IAAA,CAClB;IAEO,KAAK,CAAC,aAAa,GAAkB;QAC5C,iFAAiF;QACjF,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;QAEhE,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK;YAAE,OAAO;QAEnB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC9C,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE/C,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACxC,IAAI,CAAC,WAAW;YAAE,OAAO;QAEzB,MAAM,OAAO,GAAY;YACxB,YAAY,EAAE,YAAY;YAC1B,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;SACzE,CAAC;QAEF,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE;YACrD,MAAM;YACN,eAAe,EAAE,CAAC;YAClB,MAAM;SACN,CAAC,CAAC;QAEH,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,KAAK,EAAE,CAAC;YACX,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,KAAK,EAAE,CAAC,CAAC;QACvC,CAAC;IAAA,CACD;IAEO,KAAK,CAAC,YAAY,CAAC,MAAoB,EAAmC;QACjF,qDAAqD;QACrD,MAAM,aAAa,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;QACnD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACzC,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAE9C,IAAI,aAAa,EAAE,CAAC;YACnB,MAAM,UAAU,GAAG,MAAM,4BAA4B,CACpD,aAAa,EACb,cAAc,EACd,QAAQ,EACR,WAAW,EAAE,EAAE,EACf,MAAM,CACN,CAAC;YACF,IAAI,UAAU,CAAC,EAAE,EAAE,CAAC;gBACnB,sCAAsC;gBACtC,MAAM,SAAS,GAAG,QAAQ,CAAC,YAAY,EAAE,CAAC;gBAC1C,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,UAAU,CAAC,OAAO,CAAC,CAAC;gBACjE,IAAI,KAAK;oBAAE,OAAO,KAAK,CAAC;YACzB,CAAC;QACF,CAAC;QAED,oCAAoC;QACpC,OAAO,WAAW,CAAC;IAAA,CACnB;IAEO,qBAAqB,GAAkC;QAC9D,IAAI,CAAC;YACJ,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,EAAE,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;YAChE,MAAM,OAAO,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACjD,MAAM,MAAM,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;YAC9C,IAAI,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;gBACtC,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;YAC5B,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,+BAA+B;QAChC,CAAC;QACD,OAAO,SAAS,CAAC;IAAA,CACjB;IAEO,YAAY,GAAuB;QAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAE5C,6CAA6C;QAC7C,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;QAC1D,IAAI,CAAC,SAAS;YAAE,OAAO,SAAS,CAAC;QAEjC,MAAM,OAAO,GACZ,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ;YACpC,CAAC,CAAC,SAAS,CAAC,OAAO;YACnB,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC;gBACjC,CAAC,CAAC,SAAS,CAAC,OAAO;qBAChB,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;qBACrC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;qBACvB,IAAI,CAAC,IAAI,CAAC;gBACb,CAAC,CAAC,EAAE,CAAC;QAER,IAAI,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QAE/B,0DAAwD;QACxD,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;QACjF,OAAO,kBAAkB,SAAS,EAAE,CAAC;IAAA,CACrC;IAED,mDAAmD;IAC3C,aAAa,CAAC,QAAiB,EAAsB;QAC5D,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QAEhE,MAAM,GAAG,GAAG,QAAgE,CAAC;QAC7E,IAAI,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,SAAS,CAAC;QAElE,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;QAC5D,IAAI,CAAC,QAAQ,EAAE,IAAI;YAAE,OAAO,SAAS,CAAC;QAEtC,IAAI,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACjC,sCAAsC;QACtC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACtG,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACnC,CAAC;QACD,kBAAkB;QAClB,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9C,yBAAyB;QACzB,IAAI,KAAK,CAAC,MAAM,GAAG,gBAAgB,EAAE,CAAC;YACrC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,KAAK,IAAI,SAAS,CAAC;IAAA,CAC1B;CACD","sourcesContent":["/**\n * Auto-generates a terminal tab title from session context after a threshold\n * number of tool calls. Uses a lightweight single-shot LLM call to produce a\n * concise ≤30 character title, then sets it via the terminal's OSC 0 escape.\n *\n * Fires at most once per session. Failures are swallowed silently.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Api, Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { getPackageDir } from \"../../config.js\";\nimport type { ModelRegistry } from \"../../core/model-registry.js\";\nimport type { TabTitleSettings } from \"../../core/settings-manager.js\";\nimport { parseAgentFrontmatter, resolveModelForSubagentSpawn } from \"../../core/tools/subagent.js\";\n\nconst DEFAULT_TRIGGER_AFTER = 3;\nconst MAX_TITLE_LENGTH = 30;\nconst TITLE_GENERATION_TIMEOUT_MS = 60_000;\n\nconst TITLE_PROMPT =\n\t\"Summarize this session's task in ≤30 characters for a terminal tab title. \" +\n\t\"Output ONLY the title text, nothing else. No quotes, no explanation.\";\n\nexport interface TabTitleDeps {\n\t/** Set the terminal tab title (OSC 0). */\n\tsetTitle: (title: string) => void;\n\t/** Get the current session messages (for context). */\n\tgetMessages: () => Array<{ role: string; content?: unknown }>;\n\t/** Get the current model (parent session model — used as fallback). */\n\tgetModel: () => Model<Api> | undefined;\n\t/** Get model registry for API key resolution. */\n\tgetModelRegistry: () => ModelRegistry;\n\t/** Get the parent provider name. */\n\tgetProvider: () => string | undefined;\n}\n\nexport class TabTitleGenerator {\n\tprivate toolCallCount = 0;\n\tprivate fired = false;\n\tprivate readonly threshold: number;\n\n\tconstructor(\n\t\tprivate readonly settings: TabTitleSettings | undefined,\n\t\tprivate readonly deps: TabTitleDeps,\n\t) {\n\t\tthis.threshold = settings?.triggerAfter ?? DEFAULT_TRIGGER_AFTER;\n\t}\n\n\t/** Whether this generator is enabled. */\n\tget enabled(): boolean {\n\t\treturn this.settings?.enabled !== false;\n\t}\n\n\t/**\n\t * Called on each tool_execution_end event. Increments the counter and fires\n\t * the title generation when threshold is reached.\n\t */\n\tonToolEnd(): void {\n\t\tif (this.fired || !this.enabled) return;\n\n\t\tthis.toolCallCount++;\n\t\tif (this.toolCallCount >= this.threshold) {\n\t\t\tthis.fired = true;\n\t\t\t// Fire-and-forget — never surfaces errors to the user\n\t\t\tthis.generateTitle().catch(() => {});\n\t\t}\n\t}\n\n\t/** Exposed for testing — the current tool call count. */\n\tget currentCount(): number {\n\t\treturn this.toolCallCount;\n\t}\n\n\t/** Exposed for testing — whether the title has been generated. */\n\tget hasFired(): boolean {\n\t\treturn this.fired;\n\t}\n\n\tprivate async generateTitle(): Promise<void> {\n\t\t// Single timeout bounds the entire pipeline (model probing + API key + LLM call)\n\t\tconst signal = AbortSignal.timeout(TITLE_GENERATION_TIMEOUT_MS);\n\n\t\tconst model = await this.resolveModel(signal);\n\t\tif (!model) return;\n\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst apiKey = await registry.getApiKey(model);\n\n\t\tconst userContext = this.buildContext();\n\t\tif (!userContext) return;\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: TITLE_PROMPT,\n\t\t\tmessages: [{ role: \"user\", content: userContext, timestamp: Date.now() }],\n\t\t};\n\n\t\tconst response = await completeSimple(model, context, {\n\t\t\tapiKey,\n\t\t\tmaxRetryDelayMs: 0,\n\t\t\tsignal,\n\t\t});\n\n\t\tconst title = this.sanitizeTitle(response);\n\t\tif (title) {\n\t\t\tthis.deps.setTitle(`dreb - ${title}`);\n\t\t}\n\t}\n\n\tprivate async resolveModel(signal?: AbortSignal): Promise<Model<Api> | undefined> {\n\t\t// Try to get the Explore agent's model fallback list\n\t\tconst exploreModels = this.getExploreAgentModels();\n\t\tconst parentModel = this.deps.getModel();\n\t\tconst parentProvider = this.deps.getProvider();\n\t\tconst registry = this.deps.getModelRegistry();\n\n\t\tif (exploreModels) {\n\t\t\tconst resolution = await resolveModelForSubagentSpawn(\n\t\t\t\texploreModels,\n\t\t\t\tparentProvider,\n\t\t\t\tregistry,\n\t\t\t\tparentModel?.id,\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tif (resolution.ok) {\n\t\t\t\t// Find the resolved model in registry\n\t\t\t\tconst available = registry.getAvailable();\n\t\t\t\tconst found = available.find((m) => m.id === resolution.modelId);\n\t\t\t\tif (found) return found;\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to parent session model\n\t\treturn parentModel;\n\t}\n\n\tprivate getExploreAgentModels(): string | string[] | undefined {\n\t\ttry {\n\t\t\tconst agentFile = join(getPackageDir(), \"agents\", \"explore.md\");\n\t\t\tconst content = readFileSync(agentFile, \"utf-8\");\n\t\t\tconst parsed = parseAgentFrontmatter(content);\n\t\t\tif (parsed.ok && parsed.config.model) {\n\t\t\t\treturn parsed.config.model;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Fall through to parent model\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate buildContext(): string | undefined {\n\t\tconst messages = this.deps.getMessages();\n\t\tif (messages.length === 0) return undefined;\n\n\t\t// Extract the first user message for context\n\t\tconst firstUser = messages.find((m) => m.role === \"user\");\n\t\tif (!firstUser) return undefined;\n\n\t\tconst content =\n\t\t\ttypeof firstUser.content === \"string\"\n\t\t\t\t? firstUser.content\n\t\t\t\t: Array.isArray(firstUser.content)\n\t\t\t\t\t? firstUser.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t: \"\";\n\n\t\tif (!content) return undefined;\n\n\t\t// Truncate long messages — the LLM only needs a summary\n\t\tconst truncated = content.length > 500 ? `${content.slice(0, 500)}...` : content;\n\t\treturn `Session task:\\n${truncated}`;\n\t}\n\n\t/** Clean up LLM response to a usable tab title. */\n\tprivate sanitizeTitle(response: unknown): string | undefined {\n\t\tif (!response || typeof response !== \"object\") return undefined;\n\n\t\tconst msg = response as { content?: Array<{ type: string; text?: string }> };\n\t\tif (!msg.content || !Array.isArray(msg.content)) return undefined;\n\n\t\tconst textPart = msg.content.find((c) => c.type === \"text\");\n\t\tif (!textPart?.text) return undefined;\n\n\t\tlet title = textPart.text.trim();\n\t\t// Strip surrounding quotes if present\n\t\tif ((title.startsWith('\"') && title.endsWith('\"')) || (title.startsWith(\"'\") && title.endsWith(\"'\"))) {\n\t\t\ttitle = title.slice(1, -1).trim();\n\t\t}\n\t\t// Remove newlines\n\t\ttitle = title.replace(/[\\r\\n]+/g, \" \").trim();\n\t\t// Truncate to max length\n\t\tif (title.length > MAX_TITLE_LENGTH) {\n\t\t\ttitle = title.slice(0, MAX_TITLE_LENGTH);\n\t\t}\n\t\treturn title || undefined;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tab-title.js","sourceRoot":"","sources":["../../../src/modes/interactive/tab-title.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGjE,OAAO,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,MAAM,8BAA8B,CAAC;AAEnG,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAChC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAC5B,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAE3C,MAAM,YAAY,GACjB,8EAA4E;IAC5E,sEAAsE,CAAC;AAexE,MAAM,OAAO,iBAAiB;IAMX,QAAQ;IACR,IAAI;IANd,aAAa,GAAG,CAAC,CAAC;IAClB,KAAK,GAAG,KAAK,CAAC;IACL,SAAS,CAAS;IAEnC,YACkB,QAAsC,EACtC,IAAkB,EAClC;wBAFgB,QAAQ;oBACR,IAAI;QAErB,IAAI,CAAC,SAAS,GAAG,QAAQ,EAAE,YAAY,IAAI,qBAAqB,CAAC;IAAA,CACjE;IAED,yCAAyC;IACzC,IAAI,OAAO,GAAY;QACtB,OAAO,IAAI,CAAC,QAAQ,EAAE,OAAO,KAAK,KAAK,CAAC;IAAA,CACxC;IAED;;;OAGG;IACH,SAAS,GAAS;QACjB,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO;QAExC,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC1C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;YAClB,wDAAsD;YACtD,IAAI,CAAC,aAAa,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;QACtC,CAAC;IAAA,CACD;IAED,2DAAyD;IACzD,IAAI,YAAY,GAAW;QAC1B,OAAO,IAAI,CAAC,aAAa,CAAC;IAAA,CAC1B;IAED,oEAAkE;IAClE,IAAI,QAAQ,GAAY;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC;IAAA,CAClB;IAEO,KAAK,CAAC,aAAa,GAAkB;QAC5C,iFAAiF;QACjF,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;QAEhE,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK;YAAE,OAAO;QAEnB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC9C,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QAE/C,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACxC,IAAI,CAAC,WAAW;YAAE,OAAO;QAEzB,MAAM,OAAO,GAAY;YACxB,YAAY,EAAE,YAAY;YAC1B,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;SACzE,CAAC;QAEF,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE;YACrD,MAAM;YACN,eAAe,EAAE,CAAC;YAClB,MAAM;SACN,CAAC,CAAC;QAEH,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,KAAK,EAAE,CAAC;YACX,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,KAAK,EAAE,CAAC,CAAC;QACvC,CAAC;IAAA,CACD;IAEO,KAAK,CAAC,YAAY,CAAC,MAAoB,EAAmC;QACjF,qDAAqD;QACrD,MAAM,aAAa,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;QACnD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACzC,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAE9C,IAAI,aAAa,EAAE,CAAC;YACnB,MAAM,UAAU,GAAG,MAAM,4BAA4B,CACpD,aAAa,EACb,cAAc,EACd,QAAQ,EACR,WAAW,EAAE,EAAE,EACf,MAAM,EACN,aAAa,CACb,CAAC;YACF,IAAI,UAAU,CAAC,EAAE,EAAE,CAAC;gBACnB,sCAAsC;gBACtC,MAAM,SAAS,GAAG,QAAQ,CAAC,YAAY,EAAE,CAAC;gBAC1C,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,UAAU,CAAC,OAAO,CAAC,CAAC;gBACjE,IAAI,KAAK;oBAAE,OAAO,KAAK,CAAC;YACzB,CAAC;QACF,CAAC;QAED,oCAAoC;QACpC,OAAO,WAAW,CAAC;IAAA,CACnB;IAEO,qBAAqB,GAAkC;QAC9D,kFAAkF;QAClF,uCAAuC;QACvC,MAAM,UAAU,GAAG;YAClB,IAAI,CAAC,OAAO,EAAE,EAAE,eAAe,EAAE,QAAQ,EAAE,YAAY,CAAC;YACxD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,CAAC;YACpD,IAAI,CAAC,aAAa,EAAE,EAAE,QAAQ,EAAE,YAAY,CAAC;SAC7C,CAAC;QAEF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACpC,IAAI,CAAC;gBACJ,MAAM,OAAO,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;gBACjD,MAAM,MAAM,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;gBAC9C,IAAI,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;oBACtC,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;gBAC5B,CAAC;YACF,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;QACX,CAAC;QACD,OAAO,SAAS,CAAC;IAAA,CACjB;IAEO,YAAY,GAAuB;QAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAE5C,6CAA6C;QAC7C,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;QAC1D,IAAI,CAAC,SAAS;YAAE,OAAO,SAAS,CAAC;QAEjC,MAAM,OAAO,GACZ,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ;YACpC,CAAC,CAAC,SAAS,CAAC,OAAO;YACnB,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC;gBACjC,CAAC,CAAC,SAAS,CAAC,OAAO;qBAChB,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;qBACrC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;qBACvB,IAAI,CAAC,IAAI,CAAC;gBACb,CAAC,CAAC,EAAE,CAAC;QAER,IAAI,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QAE/B,0DAAwD;QACxD,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;QACjF,OAAO,kBAAkB,SAAS,EAAE,CAAC;IAAA,CACrC;IAED,mDAAmD;IAC3C,aAAa,CAAC,QAAiB,EAAsB;QAC5D,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QAEhE,MAAM,GAAG,GAAG,QAAgE,CAAC;QAC7E,IAAI,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,SAAS,CAAC;QAElE,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC;QAC5D,IAAI,CAAC,QAAQ,EAAE,IAAI;YAAE,OAAO,SAAS,CAAC;QAEtC,IAAI,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACjC,sCAAsC;QACtC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACtG,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACnC,CAAC;QACD,kBAAkB;QAClB,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9C,yBAAyB;QACzB,IAAI,KAAK,CAAC,MAAM,GAAG,gBAAgB,EAAE,CAAC;YACrC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,KAAK,IAAI,SAAS,CAAC;IAAA,CAC1B;CACD","sourcesContent":["/**\n * Auto-generates a terminal tab title from session context after a threshold\n * number of tool calls. Uses a lightweight single-shot LLM call to produce a\n * concise ≤30 character title, then sets it via the terminal's OSC 0 escape.\n *\n * Fires at most once per session. Failures are swallowed silently.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { Api, Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { CONFIG_DIR_NAME, getPackageDir } from \"../../config.js\";\nimport type { ModelRegistry } from \"../../core/model-registry.js\";\nimport type { TabTitleSettings } from \"../../core/settings-manager.js\";\nimport { parseAgentFrontmatter, resolveModelForSubagentSpawn } from \"../../core/tools/subagent.js\";\n\nconst DEFAULT_TRIGGER_AFTER = 3;\nconst MAX_TITLE_LENGTH = 30;\nconst TITLE_GENERATION_TIMEOUT_MS = 60_000;\n\nconst TITLE_PROMPT =\n\t\"Summarize this session's task in ≤30 characters for a terminal tab title. \" +\n\t\"Output ONLY the title text, nothing else. No quotes, no explanation.\";\n\nexport interface TabTitleDeps {\n\t/** Set the terminal tab title (OSC 0). */\n\tsetTitle: (title: string) => void;\n\t/** Get the current session messages (for context). */\n\tgetMessages: () => Array<{ role: string; content?: unknown }>;\n\t/** Get the current model (parent session model — used as fallback). */\n\tgetModel: () => Model<Api> | undefined;\n\t/** Get model registry for API key resolution. */\n\tgetModelRegistry: () => ModelRegistry;\n\t/** Get the parent provider name. */\n\tgetProvider: () => string | undefined;\n}\n\nexport class TabTitleGenerator {\n\tprivate toolCallCount = 0;\n\tprivate fired = false;\n\tprivate readonly threshold: number;\n\n\tconstructor(\n\t\tprivate readonly settings: TabTitleSettings | undefined,\n\t\tprivate readonly deps: TabTitleDeps,\n\t) {\n\t\tthis.threshold = settings?.triggerAfter ?? DEFAULT_TRIGGER_AFTER;\n\t}\n\n\t/** Whether this generator is enabled. */\n\tget enabled(): boolean {\n\t\treturn this.settings?.enabled !== false;\n\t}\n\n\t/**\n\t * Called on each tool_execution_end event. Increments the counter and fires\n\t * the title generation when threshold is reached.\n\t */\n\tonToolEnd(): void {\n\t\tif (this.fired || !this.enabled) return;\n\n\t\tthis.toolCallCount++;\n\t\tif (this.toolCallCount >= this.threshold) {\n\t\t\tthis.fired = true;\n\t\t\t// Fire-and-forget — never surfaces errors to the user\n\t\t\tthis.generateTitle().catch(() => {});\n\t\t}\n\t}\n\n\t/** Exposed for testing — the current tool call count. */\n\tget currentCount(): number {\n\t\treturn this.toolCallCount;\n\t}\n\n\t/** Exposed for testing — whether the title has been generated. */\n\tget hasFired(): boolean {\n\t\treturn this.fired;\n\t}\n\n\tprivate async generateTitle(): Promise<void> {\n\t\t// Single timeout bounds the entire pipeline (model probing + API key + LLM call)\n\t\tconst signal = AbortSignal.timeout(TITLE_GENERATION_TIMEOUT_MS);\n\n\t\tconst model = await this.resolveModel(signal);\n\t\tif (!model) return;\n\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst apiKey = await registry.getApiKey(model);\n\n\t\tconst userContext = this.buildContext();\n\t\tif (!userContext) return;\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: TITLE_PROMPT,\n\t\t\tmessages: [{ role: \"user\", content: userContext, timestamp: Date.now() }],\n\t\t};\n\n\t\tconst response = await completeSimple(model, context, {\n\t\t\tapiKey,\n\t\t\tmaxRetryDelayMs: 0,\n\t\t\tsignal,\n\t\t});\n\n\t\tconst title = this.sanitizeTitle(response);\n\t\tif (title) {\n\t\t\tthis.deps.setTitle(`dreb - ${title}`);\n\t\t}\n\t}\n\n\tprivate async resolveModel(signal?: AbortSignal): Promise<Model<Api> | undefined> {\n\t\t// Try to get the Explore agent's model fallback list\n\t\tconst exploreModels = this.getExploreAgentModels();\n\t\tconst parentModel = this.deps.getModel();\n\t\tconst parentProvider = this.deps.getProvider();\n\t\tconst registry = this.deps.getModelRegistry();\n\n\t\tif (exploreModels) {\n\t\t\tconst resolution = await resolveModelForSubagentSpawn(\n\t\t\t\texploreModels,\n\t\t\t\tparentProvider,\n\t\t\t\tregistry,\n\t\t\t\tparentModel?.id,\n\t\t\t\tsignal,\n\t\t\t\t\"[tab-title]\",\n\t\t\t);\n\t\t\tif (resolution.ok) {\n\t\t\t\t// Find the resolved model in registry\n\t\t\t\tconst available = registry.getAvailable();\n\t\t\t\tconst found = available.find((m) => m.id === resolution.modelId);\n\t\t\t\tif (found) return found;\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to parent session model\n\t\treturn parentModel;\n\t}\n\n\tprivate getExploreAgentModels(): string | string[] | undefined {\n\t\t// Resolution order mirrors discoverAgentTypes: user override > project > package.\n\t\t// First match with a valid model wins.\n\t\tconst candidates = [\n\t\t\tjoin(homedir(), CONFIG_DIR_NAME, \"agents\", \"explore.md\"),\n\t\t\tjoin(process.cwd(), \".dreb\", \"agents\", \"explore.md\"),\n\t\t\tjoin(getPackageDir(), \"agents\", \"explore.md\"),\n\t\t];\n\n\t\tfor (const agentFile of candidates) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(agentFile, \"utf-8\");\n\t\t\t\tconst parsed = parseAgentFrontmatter(content);\n\t\t\t\tif (parsed.ok && parsed.config.model) {\n\t\t\t\t\treturn parsed.config.model;\n\t\t\t\t}\n\t\t\t} catch {}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate buildContext(): string | undefined {\n\t\tconst messages = this.deps.getMessages();\n\t\tif (messages.length === 0) return undefined;\n\n\t\t// Extract the first user message for context\n\t\tconst firstUser = messages.find((m) => m.role === \"user\");\n\t\tif (!firstUser) return undefined;\n\n\t\tconst content =\n\t\t\ttypeof firstUser.content === \"string\"\n\t\t\t\t? firstUser.content\n\t\t\t\t: Array.isArray(firstUser.content)\n\t\t\t\t\t? firstUser.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t: \"\";\n\n\t\tif (!content) return undefined;\n\n\t\t// Truncate long messages — the LLM only needs a summary\n\t\tconst truncated = content.length > 500 ? `${content.slice(0, 500)}...` : content;\n\t\treturn `Session task:\\n${truncated}`;\n\t}\n\n\t/** Clean up LLM response to a usable tab title. */\n\tprivate sanitizeTitle(response: unknown): string | undefined {\n\t\tif (!response || typeof response !== \"object\") return undefined;\n\n\t\tconst msg = response as { content?: Array<{ type: string; text?: string }> };\n\t\tif (!msg.content || !Array.isArray(msg.content)) return undefined;\n\n\t\tconst textPart = msg.content.find((c) => c.type === \"text\");\n\t\tif (!textPart?.text) return undefined;\n\n\t\tlet title = textPart.text.trim();\n\t\t// Strip surrounding quotes if present\n\t\tif ((title.startsWith('\"') && title.endsWith('\"')) || (title.startsWith(\"'\") && title.endsWith(\"'\"))) {\n\t\t\ttitle = title.slice(1, -1).trim();\n\t\t}\n\t\t// Remove newlines\n\t\ttitle = title.replace(/[\\r\\n]+/g, \" \").trim();\n\t\t// Truncate to max length\n\t\tif (title.length > MAX_TITLE_LENGTH) {\n\t\t\ttitle = title.slice(0, MAX_TITLE_LENGTH);\n\t\t}\n\t\treturn title || undefined;\n\t}\n}\n"]}
|