@dreb/coding-agent 2.25.4 → 2.27.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/README.md +20 -10
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +7 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/buddy/buddy-controller.d.ts.map +1 -1
- package/dist/core/buddy/buddy-controller.js +5 -23
- package/dist/core/buddy/buddy-controller.js.map +1 -1
- package/dist/core/context-buffer.d.ts +49 -0
- package/dist/core/context-buffer.d.ts.map +1 -0
- package/dist/core/context-buffer.js +84 -0
- package/dist/core/context-buffer.js.map +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +5 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +1 -0
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +7 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +16 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +33 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +184 -87
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/tab-title.d.ts +21 -3
- package/dist/modes/interactive/tab-title.d.ts.map +1 -1
- package/dist/modes/interactive/tab-title.js +47 -25
- package/dist/modes/interactive/tab-title.js.map +1 -1
- package/docs/agent-models.md +10 -0
- package/docs/development.md +1 -1
- package/docs/providers.md +7 -0
- package/docs/settings.md +2 -2
- package/docs/tui.md +42 -0
- package/examples/extensions/custom-provider-anthropic/package.json +3 -0
- package/examples/extensions/custom-provider-gitlab-duo/package.json +3 -0
- package/examples/extensions/custom-provider-qwen-cli/package.json +3 -0
- package/examples/extensions/with-deps/package.json +3 -0
- package/package.json +2 -2
|
@@ -11,6 +11,8 @@ import type { TabTitleSettings } from "../../core/settings-manager.js";
|
|
|
11
11
|
export interface TabTitleDeps {
|
|
12
12
|
/** Set the terminal tab title (OSC 0). */
|
|
13
13
|
setTitle: (title: string) => void;
|
|
14
|
+
/** Persist the generated title as the session name. Called with the raw title (without "dreb - " prefix). */
|
|
15
|
+
setSessionName?: (name: string) => void;
|
|
14
16
|
/** Get the current session messages (for context). */
|
|
15
17
|
getMessages: () => Array<{
|
|
16
18
|
role: string;
|
|
@@ -27,6 +29,12 @@ export interface TabTitleDeps {
|
|
|
27
29
|
* Returns a non-empty fallback list when the user has configured an override.
|
|
28
30
|
*/
|
|
29
31
|
getAgentModelsOverride?: (agentName: string) => string[] | undefined;
|
|
32
|
+
/** Current git branch name, or null/undefined if unavailable. */
|
|
33
|
+
getBranch?: () => string | null | undefined;
|
|
34
|
+
/** Repository name (e.g., dirname of cwd), or undefined. */
|
|
35
|
+
getRepo?: () => string | undefined;
|
|
36
|
+
/** Current working directory, or undefined. */
|
|
37
|
+
getCwd?: () => string | undefined;
|
|
30
38
|
}
|
|
31
39
|
export declare class TabTitleGenerator {
|
|
32
40
|
private readonly settings;
|
|
@@ -34,14 +42,24 @@ export declare class TabTitleGenerator {
|
|
|
34
42
|
private toolCallCount;
|
|
35
43
|
private fired;
|
|
36
44
|
private readonly threshold;
|
|
45
|
+
private readonly contextBuffer;
|
|
37
46
|
constructor(settings: TabTitleSettings | undefined, deps: TabTitleDeps);
|
|
38
47
|
/** Whether this generator is enabled. */
|
|
39
48
|
get enabled(): boolean;
|
|
40
49
|
/**
|
|
41
|
-
* Called on each tool_execution_end event.
|
|
42
|
-
* the title generation when threshold is reached.
|
|
50
|
+
* Called on each tool_execution_end event. Captures context from the event,
|
|
51
|
+
* increments the counter, and fires title generation when threshold is reached.
|
|
43
52
|
*/
|
|
44
|
-
onToolEnd(
|
|
53
|
+
onToolEnd(event?: {
|
|
54
|
+
toolName?: string;
|
|
55
|
+
isError?: boolean;
|
|
56
|
+
result?: unknown;
|
|
57
|
+
}): void;
|
|
58
|
+
/** Called on message_end events — captures labeled context. */
|
|
59
|
+
onMessageEnd(message: {
|
|
60
|
+
role: string;
|
|
61
|
+
content?: unknown;
|
|
62
|
+
}): void;
|
|
45
63
|
/** Exposed for testing — the current tool call count. */
|
|
46
64
|
get currentCount(): number;
|
|
47
65
|
/** Exposed for testing — whether the title has been generated. */
|
|
@@ -1 +1 @@
|
|
|
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;IACtC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,EAAE,GAAG,SAAS,CAAC;CACrE;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;IA4B7B,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\t/**\n\t * Get the user's agentModels settings override for a given agent name, if any.\n\t * Returns a non-empty fallback list when the user has configured an override.\n\t */\n\tgetAgentModelsOverride?: (agentName: string) => 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// Honor the user's agentModels settings override first. The settings key must\n\t\t// match the agent name exactly (\"Explore\", as declared in explore.md frontmatter).\n\t\tconst override = this.deps.getAgentModelsOverride?.(\"Explore\");\n\t\tif (override && override.length > 0) {\n\t\t\treturn override;\n\t\t}\n\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"]}
|
|
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;AAIpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAoBvE,MAAM,WAAW,YAAY;IAC5B,0CAA0C;IAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,6GAA6G;IAC7G,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,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;IACtC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,EAAE,GAAG,SAAS,CAAC;IACrE,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC5C,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACnC,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CAClC;AAED,qBAAa,iBAAiB;IAO5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAPtB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAuB;IAErD,YACkB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,IAAI,EAAE,YAAY,EAInC;IAED,yCAAyC;IACzC,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;OAGG;IACH,SAAS,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAalF;IAED,iEAA+D;IAC/D,YAAY,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAK/D;IAED,2DAAyD;IACzD,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,oEAAkE;IAClE,IAAI,QAAQ,IAAI,OAAO,CAEtB;YAEa,aAAa;YA+Bb,YAAY;IA4B1B,OAAO,CAAC,qBAAqB;IA4B7B,OAAO,CAAC,YAAY;IAmBpB,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 { labelMessageEnd, labelToolEnd, RollingContextBuffer } from \"../../core/context-buffer.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 = 9;\nconst MAX_TITLE_LENGTH = 30;\nconst TITLE_GENERATION_TIMEOUT_MS = 60_000;\n\nconst TITLE_PROMPT =\n\t\"You are a headless terminal-tab title generator. You are NOT the assistant in the session — \" +\n\t\"you will never speak to the user. Your only job is to output a single short title string, nothing else. \" +\n\t\"No quotes, no explanation, no preamble. \" +\n\t\"The title disambiguates terminal windows for a human at a glance. \" +\n\t\"Describe what is being DONE (e.g. 'Fix auth bug', 'Plan subagent refactor', 'Review modal'), \" +\n\t\"not just label the invocation. \" +\n\t\"If a branch name is present, abbreviate it to its semantic slug \" +\n\t\"(e.g. feature/issue-217-copy-selector-modal → copy-selector) and combine with the action. \" +\n\t\"Avoid reference-only formats like '#N' or 'mach6-X #N'. \" +\n\t\"Do not include 'dreb' — the caller already adds it. \" +\n\t\"Output ONLY the title text, ≤30 characters.\";\n\nexport interface TabTitleDeps {\n\t/** Set the terminal tab title (OSC 0). */\n\tsetTitle: (title: string) => void;\n\t/** Persist the generated title as the session name. Called with the raw title (without \"dreb - \" prefix). */\n\tsetSessionName?: (name: 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\t/**\n\t * Get the user's agentModels settings override for a given agent name, if any.\n\t * Returns a non-empty fallback list when the user has configured an override.\n\t */\n\tgetAgentModelsOverride?: (agentName: string) => string[] | undefined;\n\t/** Current git branch name, or null/undefined if unavailable. */\n\tgetBranch?: () => string | null | undefined;\n\t/** Repository name (e.g., dirname of cwd), or undefined. */\n\tgetRepo?: () => string | undefined;\n\t/** Current working directory, or undefined. */\n\tgetCwd?: () => string | undefined;\n}\n\nexport class TabTitleGenerator {\n\tprivate toolCallCount = 0;\n\tprivate fired = false;\n\tprivate readonly threshold: number;\n\tprivate readonly contextBuffer: RollingContextBuffer;\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\tthis.contextBuffer = new RollingContextBuffer({ maxEntries: 30, maxChars: 6000 });\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. Captures context from the event,\n\t * increments the counter, and fires title generation when threshold is reached.\n\t */\n\tonToolEnd(event?: { toolName?: string; isError?: boolean; result?: unknown }): void {\n\t\tif (event?.toolName) {\n\t\t\tthis.contextBuffer.append(labelToolEnd(event as { toolName: string; isError?: boolean; result?: unknown }));\n\t\t}\n\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/** Called on message_end events — captures labeled context. */\n\tonMessageEnd(message: { role: string; content?: unknown }): void {\n\t\tif (this.fired) return; // no need to accumulate after fired\n\t\tfor (const entry of labelMessageEnd(message)) {\n\t\t\tthis.contextBuffer.append(entry);\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\tthis.deps.setSessionName?.(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// Honor the user's agentModels settings override first. The settings key must\n\t\t// match the agent name exactly (\"Explore\", as declared in explore.md frontmatter).\n\t\tconst override = this.deps.getAgentModelsOverride?.(\"Explore\");\n\t\tif (override && override.length > 0) {\n\t\t\treturn override;\n\t\t}\n\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 lines: string[] = [];\n\n\t\t// Metadata block\n\t\tconst branch = this.deps.getBranch?.();\n\t\tconst repo = this.deps.getRepo?.();\n\t\tconst cwd = this.deps.getCwd?.();\n\t\tif (branch) lines.push(`Branch: ${branch}`);\n\t\tif (repo) lines.push(`Repo: ${repo}`);\n\t\tif (cwd) lines.push(`Cwd: ${cwd}`);\n\n\t\t// Rolling buffer\n\t\tconst bufferContent = this.contextBuffer.build();\n\t\tif (bufferContent) lines.push(bufferContent);\n\n\t\tif (lines.length === 0) return undefined;\n\t\treturn lines.join(\"\\n\");\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"]}
|
|
@@ -10,32 +10,47 @@ import { homedir } from "node:os";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { completeSimple } from "@dreb/ai";
|
|
12
12
|
import { CONFIG_DIR_NAME, getPackageDir } from "../../config.js";
|
|
13
|
+
import { labelMessageEnd, labelToolEnd, RollingContextBuffer } from "../../core/context-buffer.js";
|
|
13
14
|
import { parseAgentFrontmatter, resolveModelForSubagentSpawn } from "../../core/tools/subagent.js";
|
|
14
|
-
const DEFAULT_TRIGGER_AFTER =
|
|
15
|
+
const DEFAULT_TRIGGER_AFTER = 9;
|
|
15
16
|
const MAX_TITLE_LENGTH = 30;
|
|
16
17
|
const TITLE_GENERATION_TIMEOUT_MS = 60_000;
|
|
17
|
-
const TITLE_PROMPT = "
|
|
18
|
-
"
|
|
18
|
+
const TITLE_PROMPT = "You are a headless terminal-tab title generator. You are NOT the assistant in the session — " +
|
|
19
|
+
"you will never speak to the user. Your only job is to output a single short title string, nothing else. " +
|
|
20
|
+
"No quotes, no explanation, no preamble. " +
|
|
21
|
+
"The title disambiguates terminal windows for a human at a glance. " +
|
|
22
|
+
"Describe what is being DONE (e.g. 'Fix auth bug', 'Plan subagent refactor', 'Review modal'), " +
|
|
23
|
+
"not just label the invocation. " +
|
|
24
|
+
"If a branch name is present, abbreviate it to its semantic slug " +
|
|
25
|
+
"(e.g. feature/issue-217-copy-selector-modal → copy-selector) and combine with the action. " +
|
|
26
|
+
"Avoid reference-only formats like '#N' or 'mach6-X #N'. " +
|
|
27
|
+
"Do not include 'dreb' — the caller already adds it. " +
|
|
28
|
+
"Output ONLY the title text, ≤30 characters.";
|
|
19
29
|
export class TabTitleGenerator {
|
|
20
30
|
settings;
|
|
21
31
|
deps;
|
|
22
32
|
toolCallCount = 0;
|
|
23
33
|
fired = false;
|
|
24
34
|
threshold;
|
|
35
|
+
contextBuffer;
|
|
25
36
|
constructor(settings, deps) {
|
|
26
37
|
this.settings = settings;
|
|
27
38
|
this.deps = deps;
|
|
28
39
|
this.threshold = settings?.triggerAfter ?? DEFAULT_TRIGGER_AFTER;
|
|
40
|
+
this.contextBuffer = new RollingContextBuffer({ maxEntries: 30, maxChars: 6000 });
|
|
29
41
|
}
|
|
30
42
|
/** Whether this generator is enabled. */
|
|
31
43
|
get enabled() {
|
|
32
44
|
return this.settings?.enabled !== false;
|
|
33
45
|
}
|
|
34
46
|
/**
|
|
35
|
-
* Called on each tool_execution_end event.
|
|
36
|
-
* the title generation when threshold is reached.
|
|
47
|
+
* Called on each tool_execution_end event. Captures context from the event,
|
|
48
|
+
* increments the counter, and fires title generation when threshold is reached.
|
|
37
49
|
*/
|
|
38
|
-
onToolEnd() {
|
|
50
|
+
onToolEnd(event) {
|
|
51
|
+
if (event?.toolName) {
|
|
52
|
+
this.contextBuffer.append(labelToolEnd(event));
|
|
53
|
+
}
|
|
39
54
|
if (this.fired || !this.enabled)
|
|
40
55
|
return;
|
|
41
56
|
this.toolCallCount++;
|
|
@@ -45,6 +60,14 @@ export class TabTitleGenerator {
|
|
|
45
60
|
this.generateTitle().catch(() => { });
|
|
46
61
|
}
|
|
47
62
|
}
|
|
63
|
+
/** Called on message_end events — captures labeled context. */
|
|
64
|
+
onMessageEnd(message) {
|
|
65
|
+
if (this.fired)
|
|
66
|
+
return; // no need to accumulate after fired
|
|
67
|
+
for (const entry of labelMessageEnd(message)) {
|
|
68
|
+
this.contextBuffer.append(entry);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
48
71
|
/** Exposed for testing — the current tool call count. */
|
|
49
72
|
get currentCount() {
|
|
50
73
|
return this.toolCallCount;
|
|
@@ -76,6 +99,7 @@ export class TabTitleGenerator {
|
|
|
76
99
|
const title = this.sanitizeTitle(response);
|
|
77
100
|
if (title) {
|
|
78
101
|
this.deps.setTitle(`dreb - ${title}`);
|
|
102
|
+
this.deps.setSessionName?.(title);
|
|
79
103
|
}
|
|
80
104
|
}
|
|
81
105
|
async resolveModel(signal) {
|
|
@@ -124,26 +148,24 @@ export class TabTitleGenerator {
|
|
|
124
148
|
return undefined;
|
|
125
149
|
}
|
|
126
150
|
buildContext() {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
151
|
+
const lines = [];
|
|
152
|
+
// Metadata block
|
|
153
|
+
const branch = this.deps.getBranch?.();
|
|
154
|
+
const repo = this.deps.getRepo?.();
|
|
155
|
+
const cwd = this.deps.getCwd?.();
|
|
156
|
+
if (branch)
|
|
157
|
+
lines.push(`Branch: ${branch}`);
|
|
158
|
+
if (repo)
|
|
159
|
+
lines.push(`Repo: ${repo}`);
|
|
160
|
+
if (cwd)
|
|
161
|
+
lines.push(`Cwd: ${cwd}`);
|
|
162
|
+
// Rolling buffer
|
|
163
|
+
const bufferContent = this.contextBuffer.build();
|
|
164
|
+
if (bufferContent)
|
|
165
|
+
lines.push(bufferContent);
|
|
166
|
+
if (lines.length === 0)
|
|
143
167
|
return undefined;
|
|
144
|
-
|
|
145
|
-
const truncated = content.length > 500 ? `${content.slice(0, 500)}...` : content;
|
|
146
|
-
return `Session task:\n${truncated}`;
|
|
168
|
+
return lines.join("\n");
|
|
147
169
|
}
|
|
148
170
|
/** Clean up LLM response to a usable tab title. */
|
|
149
171
|
sanitizeTitle(response) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-title.js","sourceRoot":"","sources":["../../../src/modes/interactive/tab-title.ts"],"names":[],"mappings":"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","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\t/**\n\t * Get the user's agentModels settings override for a given agent name, if any.\n\t * Returns a non-empty fallback list when the user has configured an override.\n\t */\n\tgetAgentModelsOverride?: (agentName: string) => 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// Honor the user's agentModels settings override first. The settings key must\n\t\t// match the agent name exactly (\"Explore\", as declared in explore.md frontmatter).\n\t\tconst override = this.deps.getAgentModelsOverride?.(\"Explore\");\n\t\tif (override && override.length > 0) {\n\t\t\treturn override;\n\t\t}\n\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"]}
|
|
1
|
+
{"version":3,"file":"tab-title.js","sourceRoot":"","sources":["../../../src/modes/interactive/tab-title.ts"],"names":[],"mappings":"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","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 { labelMessageEnd, labelToolEnd, RollingContextBuffer } from \"../../core/context-buffer.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 = 9;\nconst MAX_TITLE_LENGTH = 30;\nconst TITLE_GENERATION_TIMEOUT_MS = 60_000;\n\nconst TITLE_PROMPT =\n\t\"You are a headless terminal-tab title generator. You are NOT the assistant in the session — \" +\n\t\"you will never speak to the user. Your only job is to output a single short title string, nothing else. \" +\n\t\"No quotes, no explanation, no preamble. \" +\n\t\"The title disambiguates terminal windows for a human at a glance. \" +\n\t\"Describe what is being DONE (e.g. 'Fix auth bug', 'Plan subagent refactor', 'Review modal'), \" +\n\t\"not just label the invocation. \" +\n\t\"If a branch name is present, abbreviate it to its semantic slug \" +\n\t\"(e.g. feature/issue-217-copy-selector-modal → copy-selector) and combine with the action. \" +\n\t\"Avoid reference-only formats like '#N' or 'mach6-X #N'. \" +\n\t\"Do not include 'dreb' — the caller already adds it. \" +\n\t\"Output ONLY the title text, ≤30 characters.\";\n\nexport interface TabTitleDeps {\n\t/** Set the terminal tab title (OSC 0). */\n\tsetTitle: (title: string) => void;\n\t/** Persist the generated title as the session name. Called with the raw title (without \"dreb - \" prefix). */\n\tsetSessionName?: (name: 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\t/**\n\t * Get the user's agentModels settings override for a given agent name, if any.\n\t * Returns a non-empty fallback list when the user has configured an override.\n\t */\n\tgetAgentModelsOverride?: (agentName: string) => string[] | undefined;\n\t/** Current git branch name, or null/undefined if unavailable. */\n\tgetBranch?: () => string | null | undefined;\n\t/** Repository name (e.g., dirname of cwd), or undefined. */\n\tgetRepo?: () => string | undefined;\n\t/** Current working directory, or undefined. */\n\tgetCwd?: () => string | undefined;\n}\n\nexport class TabTitleGenerator {\n\tprivate toolCallCount = 0;\n\tprivate fired = false;\n\tprivate readonly threshold: number;\n\tprivate readonly contextBuffer: RollingContextBuffer;\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\tthis.contextBuffer = new RollingContextBuffer({ maxEntries: 30, maxChars: 6000 });\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. Captures context from the event,\n\t * increments the counter, and fires title generation when threshold is reached.\n\t */\n\tonToolEnd(event?: { toolName?: string; isError?: boolean; result?: unknown }): void {\n\t\tif (event?.toolName) {\n\t\t\tthis.contextBuffer.append(labelToolEnd(event as { toolName: string; isError?: boolean; result?: unknown }));\n\t\t}\n\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/** Called on message_end events — captures labeled context. */\n\tonMessageEnd(message: { role: string; content?: unknown }): void {\n\t\tif (this.fired) return; // no need to accumulate after fired\n\t\tfor (const entry of labelMessageEnd(message)) {\n\t\t\tthis.contextBuffer.append(entry);\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\tthis.deps.setSessionName?.(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// Honor the user's agentModels settings override first. The settings key must\n\t\t// match the agent name exactly (\"Explore\", as declared in explore.md frontmatter).\n\t\tconst override = this.deps.getAgentModelsOverride?.(\"Explore\");\n\t\tif (override && override.length > 0) {\n\t\t\treturn override;\n\t\t}\n\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 lines: string[] = [];\n\n\t\t// Metadata block\n\t\tconst branch = this.deps.getBranch?.();\n\t\tconst repo = this.deps.getRepo?.();\n\t\tconst cwd = this.deps.getCwd?.();\n\t\tif (branch) lines.push(`Branch: ${branch}`);\n\t\tif (repo) lines.push(`Repo: ${repo}`);\n\t\tif (cwd) lines.push(`Cwd: ${cwd}`);\n\n\t\t// Rolling buffer\n\t\tconst bufferContent = this.contextBuffer.build();\n\t\tif (bufferContent) lines.push(bufferContent);\n\n\t\tif (lines.length === 0) return undefined;\n\t\treturn lines.join(\"\\n\");\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"]}
|
package/docs/agent-models.md
CHANGED
|
@@ -36,6 +36,16 @@ When a subagent is launched, its model is resolved in this priority:
|
|
|
36
36
|
|
|
37
37
|
If the `agentModels.models` list is empty or undefined for a given agent, resolution falls through to the agent definition's model, then to the parent session model.
|
|
38
38
|
|
|
39
|
+
## Parent Model Identity in System Prompt
|
|
40
|
+
|
|
41
|
+
The **parent session's** running model is exposed in its own system prompt as:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
You are running on: provider/id
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This lets the parent model make self-aware routing decisions — e.g. delegating vision tasks if it's on a text-only model, or explicitly requesting a differently-architected model as a critic when diverse perspectives improve reliability. The identity line updates automatically whenever the user switches models mid-session.
|
|
48
|
+
|
|
39
49
|
## TUI Usage
|
|
40
50
|
|
|
41
51
|
Open `/settings` and select the **Agent Models** submenu. Each discovered agent type gets its own entry where you can:
|
package/docs/development.md
CHANGED
package/docs/providers.md
CHANGED
|
@@ -4,6 +4,7 @@ dreb supports subscription-based providers via OAuth and API key providers via e
|
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
7
|
+
- [Node Version and Streaming](#node-version-and-streaming)
|
|
7
8
|
- [Subscriptions](#subscriptions)
|
|
8
9
|
- [API Keys](#api-keys)
|
|
9
10
|
- [Auth File](#auth-file)
|
|
@@ -11,6 +12,12 @@ dreb supports subscription-based providers via OAuth and API key providers via e
|
|
|
11
12
|
- [Custom Providers](#custom-providers)
|
|
12
13
|
- [Resolution Order](#resolution-order)
|
|
13
14
|
|
|
15
|
+
## Node Version and Streaming
|
|
16
|
+
|
|
17
|
+
**Use Node.js 22 LTS.** dreb's providers rely on stable SSE streaming, and Node 22 LTS is the supported runtime. Node 24 and Node 26 are known to break streaming: Node 26 changed ReadableStream buffering to "read one buffer at a time instead of reading ahead", which breaks the SSE stream parsers in the Anthropic SDK (v0.73.0) and OpenAI SDK (v6.26.0) that dreb uses.
|
|
18
|
+
|
|
19
|
+
If every provider fails with **"request ended without sending any chunks"**, check your Node version and switch to Node.js 22 LTS.
|
|
20
|
+
|
|
14
21
|
## Subscriptions
|
|
15
22
|
|
|
16
23
|
Use `/login` in interactive mode, then select a provider:
|
package/docs/settings.md
CHANGED
|
@@ -97,7 +97,7 @@ current model supports adaptive thinking).
|
|
|
97
97
|
| Setting | Type | Default | Description |
|
|
98
98
|
|---------|------|---------|-------------|
|
|
99
99
|
| `tabTitle.enabled` | boolean | `true` | Auto-generate terminal tab title from session task |
|
|
100
|
-
| `tabTitle.triggerAfter` | number | `
|
|
100
|
+
| `tabTitle.triggerAfter` | number | `9` | Number of tool calls before generating title |
|
|
101
101
|
|
|
102
102
|
After the configured number of tool calls, dreb fires a single background LLM call to summarize the session's task into a short (≤30 character) terminal tab title, then sets it via OSC 0. Only fires once per session. If the LLM call fails, the default title remains.
|
|
103
103
|
|
|
@@ -105,7 +105,7 @@ After the configured number of tool calls, dreb fires a single background LLM ca
|
|
|
105
105
|
{
|
|
106
106
|
"tabTitle": {
|
|
107
107
|
"enabled": true,
|
|
108
|
-
"triggerAfter":
|
|
108
|
+
"triggerAfter": 9
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
```
|
package/docs/tui.md
CHANGED
|
@@ -443,6 +443,48 @@ interface MyTheme {
|
|
|
443
443
|
}
|
|
444
444
|
```
|
|
445
445
|
|
|
446
|
+
## Committed-scrollback + live-region model
|
|
447
|
+
|
|
448
|
+
The TUI uses a two-zone rendering architecture:
|
|
449
|
+
|
|
450
|
+
- **Committed region** — the first N children of the TUI root (set via `setCommittedChildCount()`). Their output is written to terminal scrollback once and **never re-rendered** by the differential renderer. This prevents the "transcript replay" problem where every turn-end would re-emit the entire session into scrollback.
|
|
451
|
+
- **Live region** — all children after the committed boundary. This is the only content the differential renderer manages. Full redraws are cheap because they only clear and rewrite the small live region (streaming message + spinner + editor + footer).
|
|
452
|
+
|
|
453
|
+
### Key methods
|
|
454
|
+
|
|
455
|
+
| Method | Purpose |
|
|
456
|
+
|--------|---------|
|
|
457
|
+
| `setCommittedChildCount(n)` | Mark the first N children as committed |
|
|
458
|
+
| `commit()` | Update line tracking after components move into committed containers |
|
|
459
|
+
| `recommitAll()` | Clear screen + scrollback, re-render everything, re-establish boundary. Used for global actions (theme change, width resize, expand-all, show-images, hide-thinking, session switch) |
|
|
460
|
+
|
|
461
|
+
### How it works
|
|
462
|
+
|
|
463
|
+
1. `interactive-mode.ts` maintains a `committedChatContainer` (finalized messages/tools) and a live `chatContainer` (streaming + pending)
|
|
464
|
+
2. When a message or tool finalizes, it moves from live → committed container, and `commit()` advances the boundary
|
|
465
|
+
3. The differential renderer (`doRender`) only renders live children, so the "content shrank" full-redraw path (triggered by spinner removal) only replays the live region — not the whole transcript
|
|
466
|
+
4. Terminal width changes and other global mutations call `recommitAll()` for one deliberate repaint
|
|
467
|
+
|
|
468
|
+
### Prefix-commit ordering rule
|
|
469
|
+
|
|
470
|
+
Because scrollback is append-only, components must commit in display order. If tool 3 finishes before tool 2, tool 3 cannot be committed until tool 2 finishes and commits first. Only the leading contiguous run of fully-finalized components may advance into scrollback.
|
|
471
|
+
|
|
472
|
+
### Transient inline UI (autocomplete menu)
|
|
473
|
+
|
|
474
|
+
Inline UI that expands and collapses inside the live region — such as the editor's autocomplete/slash-command menu — needs special handling, because terminals can only scroll content *up* into scrollback, never pull it back *down*. When such a menu opens it grows the live region and pushes committed content up into scrollback; a later shrink (filtering to fewer matches, or dismissing) cannot restore that committed content with a relative redraw, leaving ghost whitespace below the prompt.
|
|
475
|
+
|
|
476
|
+
Two complementary measures prevent this:
|
|
477
|
+
|
|
478
|
+
1. **Stable height while open** — the menu block is padded to a per-session high-water mark, so filtering to fewer matches never shrinks the live region. Filtering only repaints in place.
|
|
479
|
+
2. **`recommitAll()` on close** — dismissing or accepting the menu repaints the whole transcript with position-independent sequences, restoring the committed content that scrolled off. This is safe because such menus only appear while the user is typing at the bottom with no agent streaming.
|
|
480
|
+
|
|
481
|
+
### Environment variables
|
|
482
|
+
|
|
483
|
+
| Variable | Purpose |
|
|
484
|
+
|----------|---------|
|
|
485
|
+
| `DREB_DEBUG_REDRAW=1` | Log every full-redraw trigger to `~/.dreb/agent/dreb-debug.log` |
|
|
486
|
+
| `DREB_TUI_DEBUG=1` | Write full render state to `/tmp/tui/` on each differential render |
|
|
487
|
+
|
|
446
488
|
## Debug logging
|
|
447
489
|
|
|
448
490
|
Set `DREB_TUI_WRITE_LOG` to capture the raw ANSI stream written to stdout.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreb/coding-agent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.27.2",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"drebConfig": {
|
|
@@ -120,6 +120,6 @@
|
|
|
120
120
|
"directory": "packages/coding-agent"
|
|
121
121
|
},
|
|
122
122
|
"engines": {
|
|
123
|
-
"node": "
|
|
123
|
+
"node": "^22.0.0"
|
|
124
124
|
}
|
|
125
125
|
}
|