@kolisachint/hoocode-agent 0.5.47 → 0.5.49
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 +19 -0
- package/dist/core/agent-session-retry.d.ts.map +1 -1
- package/dist/core/agent-session-retry.js +10 -1
- package/dist/core/agent-session-retry.js.map +1 -1
- package/dist/core/session-identity.d.ts +10 -0
- package/dist/core/session-identity.d.ts.map +1 -1
- package/dist/core/session-identity.js +48 -0
- package/dist/core/session-identity.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +7 -3
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +156 -65
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +5 -4
- package/dist/core/tools/read.js.map +1 -1
- package/dist/modes/interactive/command-executor.d.ts +9 -3
- package/dist/modes/interactive/command-executor.d.ts.map +1 -1
- package/dist/modes/interactive/command-executor.js +16 -8
- package/dist/modes/interactive/command-executor.js.map +1 -1
- package/dist/modes/interactive/components/session-color-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-color-selector.js +5 -2
- package/dist/modes/interactive/components/session-color-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +7 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/sleep.d.ts +9 -0
- package/dist/utils/sleep.d.ts.map +1 -1
- package/dist/utils/sleep.js +11 -1
- package/dist/utils/sleep.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.49] - 2026-09-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `/color` takes colour names, not just slot numbers: `/color green`,
|
|
8
|
+
`/color blue`, `/color magenta`, and the first letter of each on its own —
|
|
9
|
+
`/color g`, `/color b`, `/color y`, `/color r`.
|
|
10
|
+
|
|
11
|
+
The six slots are palette positions rather than fixed hues, but across the
|
|
12
|
+
built-in themes each position keeps a recognisable family, so the names
|
|
13
|
+
describe it: cyan, purple, yellow, magenta, green, blue. Spellings that sit
|
|
14
|
+
between slots resolve to the nearest one instead of being rejected — `red`
|
|
15
|
+
and `pink` reach magenta, `orange` and `amber` reach yellow — and the
|
|
16
|
+
confirmation names the slot it landed on, so `/color red` answering with
|
|
17
|
+
"magenta" is how you learn the palette has no red of its own. The numbers
|
|
18
|
+
still work, and the picker now labels each swatch with its name.
|
|
19
|
+
|
|
20
|
+
## [0.5.48] - 2026-09-03
|
|
21
|
+
|
|
3
22
|
## [0.5.47] - 2026-08-31
|
|
4
23
|
|
|
5
24
|
### Added
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session-retry.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAGvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAU5D,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC7B,gBAAgB,IAAI;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAClF,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,gBAAgB,IAAI,YAAY,EAAE,CAAC;IACnC,gBAAgB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IACjD,sFAAsF;IACtF,aAAa,IAAI,IAAI,CAAC;IACtB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACrC;AAED,qBAAa,mBAAmB;IAMnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,gBAAgB,CAA0C;IAClE,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,QAAQ,CAAuC;IAEvD,YAA6B,IAAI,EAAE,aAAa,EAAI;IAEpD,gDAAgD;IAChD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,+CAA+C;IAC/C,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"agent-session-retry.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAGvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAU5D,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC7B,gBAAgB,IAAI;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAClF,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,gBAAgB,IAAI,YAAY,EAAE,CAAC;IACnC,gBAAgB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IACjD,sFAAsF;IACtF,aAAa,IAAI,IAAI,CAAC;IACtB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACrC;AAED,qBAAa,mBAAmB;IAMnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,gBAAgB,CAA0C;IAClE,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,QAAQ,CAAuC;IAEvD,YAA6B,IAAI,EAAE,aAAa,EAAI;IAEpD,gDAAgD;IAChD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,+CAA+C;IAC/C,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAiBnD;IAED;;;;;;OAMG;IACH,wBAAwB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAkBhD;IAED,OAAO,CAAC,4BAA4B;IAUpC;;;;OAIG;IACH,6BAA6B,IAAI,IAAI,CASpC;IAED,wCAAwC;IACxC,OAAO,IAAI,IAAI,CAMd;IAED;;;OAGG;IACG,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwEtE;IAED;;OAEG;IACH,KAAK,IAAI,IAAI,CAIZ;IAED;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAOlC;CACD","sourcesContent":["/**\n * Auto-retry controller for AgentSession.\n *\n * Owns the retry lifecycle for transient assistant errors (overloaded, rate\n * limit, server/network/transport failures): deciding whether an error is\n * retryable, arming the retry promise synchronously on agent_end, backing off\n * exponentially, and re-driving the agent via continue(). Context-overflow\n * errors are intentionally excluded here — those are handled by compaction.\n */\n\nimport type { AgentEvent, AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport { isContextOverflow, isLongRetryDelayError } from \"@kolisachint/hoocode-ai\";\nimport { sleep } from \"../utils/sleep.js\";\nimport type { AgentSessionEvent } from \"./agent-session.js\";\n\n/**\n * Retryable error signatures (overloaded, rate limit, server/network errors,\n * transport closes). Compiled once at module load instead of on every assistant\n * response. Context-overflow errors are handled separately by compaction.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;\n\n/** Narrow dependencies the retry controller needs from AgentSession. */\nexport interface AutoRetryDeps {\n\tgetRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number };\n\tgetModel(): Model<any> | undefined;\n\tgetAgentMessages(): AgentMessage[];\n\tsetAgentMessages(messages: AgentMessage[]): void;\n\t/** Fire-and-forget continue() on the agent (errors surface on the next agent_end). */\n\tcontinueAgent(): void;\n\twaitForAgentIdle(): Promise<void>;\n\temit(event: AgentSessionEvent): void;\n}\n\nexport class AutoRetryController {\n\tprivate _abortController: AbortController | undefined = undefined;\n\tprivate _attempt = 0;\n\tprivate _promise: Promise<void> | undefined = undefined;\n\tprivate _resolve: (() => void) | undefined = undefined;\n\n\tconstructor(private readonly deps: AutoRetryDeps) {}\n\n\t/** Current retry attempt (0 if not retrying) */\n\tget attempt(): number {\n\t\treturn this._attempt;\n\t}\n\n\t/** Whether a retry is currently in progress */\n\tget isRetrying(): boolean {\n\t\treturn this._promise !== undefined;\n\t}\n\n\t/**\n\t * Check if an error is retryable (overloaded, rate limit, server errors).\n\t * Context overflow errors are NOT retryable (handled by compaction instead).\n\t */\n\tisRetryableError(message: AssistantMessage): boolean {\n\t\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\n\t\t// Context overflow is handled by compaction, not retry\n\t\tconst contextWindow = this.deps.getModel()?.contextWindow ?? 0;\n\t\tif (isContextOverflow(message, contextWindow)) return false;\n\n\t\t// An exhausted quota answers 429 the same way a burst rate limit does,\n\t\t// and the pattern below cannot tell them apart — both say \"429\". The\n\t\t// difference is in how long the provider asked us to wait: seconds mean\n\t\t// try again, weeks mean this session is not getting through no matter\n\t\t// how patiently it backs off. Retrying the second kind spends the whole\n\t\t// budget in a few seconds and reports failure as if the network were at\n\t\t// fault.\n\t\tif (isLongRetryDelayError(message.errorMessage)) return false;\n\n\t\treturn RETRYABLE_ERROR_PATTERN.test(message.errorMessage);\n\t}\n\n\t/**\n\t * Create the retry promise synchronously when an agent_end carries a\n\t * retryable error. Agent.emit() runs handlers synchronously and prompt()\n\t * calls waitForRetry() as soon as agent.prompt() resolves; arming the promise\n\t * here (rather than inside async event processing) ensures waitForRetry()\n\t * never misses an in-flight retry.\n\t */\n\tcreatePromiseForAgentEnd(event: AgentEvent): void {\n\t\tif (event.type !== \"agent_end\" || this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst lastAssistant = this._findLastAssistantInMessages(event.messages);\n\t\tif (!lastAssistant || !this.isRetryableError(lastAssistant)) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis._promise = new Promise((resolve) => {\n\t\t\tthis._resolve = resolve;\n\t\t});\n\t}\n\n\tprivate _findLastAssistantInMessages(messages: AgentMessage[]): AssistantMessage | undefined {\n\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\tconst message = messages[i];\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\treturn message as AssistantMessage;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Reset the attempt counter after a successful assistant response.\n\t * Callers invoke this only for non-error responses; it emits a success event\n\t * when a retry was in progress.\n\t */\n\tonSuccessfulAssistantResponse(): void {\n\t\tif (this._attempt > 0) {\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: true,\n\t\t\t\tattempt: this._attempt,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t}\n\t}\n\n\t/** Resolve the pending retry promise */\n\tresolve(): void {\n\t\tif (this._resolve) {\n\t\t\tthis._resolve();\n\t\t\tthis._resolve = undefined;\n\t\t\tthis._promise = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Handle retryable errors with exponential backoff.\n\t * @returns true if retry was initiated, false if max retries exceeded or disabled\n\t */\n\tasync handleRetryableError(message: AssistantMessage): Promise<boolean> {\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\n\t\t// Retry promise is created synchronously in createPromiseForAgentEnd for agent_end.\n\t\t// Keep a defensive fallback here in case a future refactor bypasses that path.\n\t\tif (!this._promise) {\n\t\t\tthis._promise = new Promise((resolve) => {\n\t\t\t\tthis._resolve = resolve;\n\t\t\t});\n\t\t}\n\n\t\tthis._attempt++;\n\n\t\tif (this._attempt > settings.maxRetries) {\n\t\t\t// Max retries exceeded, emit final failure and reset\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt: this._attempt - 1,\n\t\t\t\tfinalError: message.errorMessage,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t\tthis.resolve(); // Resolve so waitForRetry() completes\n\t\t\treturn false;\n\t\t}\n\n\t\tconst delayMs = settings.baseDelayMs * 2 ** (this._attempt - 1);\n\n\t\tthis.deps.emit({\n\t\t\ttype: \"auto_retry_start\",\n\t\t\tattempt: this._attempt,\n\t\t\tmaxAttempts: settings.maxRetries,\n\t\t\tdelayMs,\n\t\t\terrorMessage: message.errorMessage || \"Unknown error\",\n\t\t});\n\n\t\t// Remove error message from agent state (keep in session for history)\n\t\tconst messages = this.deps.getAgentMessages();\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.deps.setAgentMessages(messages.slice(0, -1));\n\t\t}\n\n\t\t// Wait with exponential backoff (abortable)\n\t\tthis._abortController = new AbortController();\n\t\ttry {\n\t\t\tawait sleep(delayMs, this._abortController.signal);\n\t\t} catch {\n\t\t\t// Aborted during sleep - emit end event so UI can clean up\n\t\t\tconst attempt = this._attempt;\n\t\t\tthis._attempt = 0;\n\t\t\tthis._abortController = undefined;\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt,\n\t\t\t\tfinalError: \"Retry cancelled\",\n\t\t\t});\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\t\tthis._abortController = undefined;\n\n\t\t// Retry via continue() - use setTimeout to break out of event handler chain\n\t\tsetTimeout(() => {\n\t\t\tthis.deps.continueAgent();\n\t\t}, 0);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Cancel in-progress retry.\n\t */\n\tabort(): void {\n\t\tthis._abortController?.abort();\n\t\t// Note: _attempt is reset in the catch block of handleRetryableError\n\t\tthis.resolve();\n\t}\n\n\t/**\n\t * Wait for any in-progress retry to complete.\n\t * Returns immediately if no retry is in progress.\n\t */\n\tasync waitForRetry(): Promise<void> {\n\t\tif (!this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait this._promise;\n\t\tawait this.deps.waitForAgentIdle();\n\t}\n}\n"]}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* exponentially, and re-driving the agent via continue(). Context-overflow
|
|
8
8
|
* errors are intentionally excluded here — those are handled by compaction.
|
|
9
9
|
*/
|
|
10
|
-
import { isContextOverflow } from "@kolisachint/hoocode-ai";
|
|
10
|
+
import { isContextOverflow, isLongRetryDelayError } from "@kolisachint/hoocode-ai";
|
|
11
11
|
import { sleep } from "../utils/sleep.js";
|
|
12
12
|
/**
|
|
13
13
|
* Retryable error signatures (overloaded, rate limit, server/network errors,
|
|
@@ -43,6 +43,15 @@ export class AutoRetryController {
|
|
|
43
43
|
const contextWindow = this.deps.getModel()?.contextWindow ?? 0;
|
|
44
44
|
if (isContextOverflow(message, contextWindow))
|
|
45
45
|
return false;
|
|
46
|
+
// An exhausted quota answers 429 the same way a burst rate limit does,
|
|
47
|
+
// and the pattern below cannot tell them apart — both say "429". The
|
|
48
|
+
// difference is in how long the provider asked us to wait: seconds mean
|
|
49
|
+
// try again, weeks mean this session is not getting through no matter
|
|
50
|
+
// how patiently it backs off. Retrying the second kind spends the whole
|
|
51
|
+
// budget in a few seconds and reports failure as if the network were at
|
|
52
|
+
// fault.
|
|
53
|
+
if (isLongRetryDelayError(message.errorMessage))
|
|
54
|
+
return false;
|
|
46
55
|
return RETRYABLE_ERROR_PATTERN.test(message.errorMessage);
|
|
47
56
|
}
|
|
48
57
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session-retry.js","sourceRoot":"","sources":["../../src/core/agent-session-retry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Auto-retry controller for AgentSession.\n *\n * Owns the retry lifecycle for transient assistant errors (overloaded, rate\n * limit, server/network/transport failures): deciding whether an error is\n * retryable, arming the retry promise synchronously on agent_end, backing off\n * exponentially, and re-driving the agent via continue(). Context-overflow\n * errors are intentionally excluded here — those are handled by compaction.\n */\n\nimport type { AgentEvent, AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport { isContextOverflow } from \"@kolisachint/hoocode-ai\";\nimport { sleep } from \"../utils/sleep.js\";\nimport type { AgentSessionEvent } from \"./agent-session.js\";\n\n/**\n * Retryable error signatures (overloaded, rate limit, server/network errors,\n * transport closes). Compiled once at module load instead of on every assistant\n * response. Context-overflow errors are handled separately by compaction.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;\n\n/** Narrow dependencies the retry controller needs from AgentSession. */\nexport interface AutoRetryDeps {\n\tgetRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number };\n\tgetModel(): Model<any> | undefined;\n\tgetAgentMessages(): AgentMessage[];\n\tsetAgentMessages(messages: AgentMessage[]): void;\n\t/** Fire-and-forget continue() on the agent (errors surface on the next agent_end). */\n\tcontinueAgent(): void;\n\twaitForAgentIdle(): Promise<void>;\n\temit(event: AgentSessionEvent): void;\n}\n\nexport class AutoRetryController {\n\tprivate _abortController: AbortController | undefined = undefined;\n\tprivate _attempt = 0;\n\tprivate _promise: Promise<void> | undefined = undefined;\n\tprivate _resolve: (() => void) | undefined = undefined;\n\n\tconstructor(private readonly deps: AutoRetryDeps) {}\n\n\t/** Current retry attempt (0 if not retrying) */\n\tget attempt(): number {\n\t\treturn this._attempt;\n\t}\n\n\t/** Whether a retry is currently in progress */\n\tget isRetrying(): boolean {\n\t\treturn this._promise !== undefined;\n\t}\n\n\t/**\n\t * Check if an error is retryable (overloaded, rate limit, server errors).\n\t * Context overflow errors are NOT retryable (handled by compaction instead).\n\t */\n\tisRetryableError(message: AssistantMessage): boolean {\n\t\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\n\t\t// Context overflow is handled by compaction, not retry\n\t\tconst contextWindow = this.deps.getModel()?.contextWindow ?? 0;\n\t\tif (isContextOverflow(message, contextWindow)) return false;\n\n\t\treturn RETRYABLE_ERROR_PATTERN.test(message.errorMessage);\n\t}\n\n\t/**\n\t * Create the retry promise synchronously when an agent_end carries a\n\t * retryable error. Agent.emit() runs handlers synchronously and prompt()\n\t * calls waitForRetry() as soon as agent.prompt() resolves; arming the promise\n\t * here (rather than inside async event processing) ensures waitForRetry()\n\t * never misses an in-flight retry.\n\t */\n\tcreatePromiseForAgentEnd(event: AgentEvent): void {\n\t\tif (event.type !== \"agent_end\" || this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst lastAssistant = this._findLastAssistantInMessages(event.messages);\n\t\tif (!lastAssistant || !this.isRetryableError(lastAssistant)) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis._promise = new Promise((resolve) => {\n\t\t\tthis._resolve = resolve;\n\t\t});\n\t}\n\n\tprivate _findLastAssistantInMessages(messages: AgentMessage[]): AssistantMessage | undefined {\n\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\tconst message = messages[i];\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\treturn message as AssistantMessage;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Reset the attempt counter after a successful assistant response.\n\t * Callers invoke this only for non-error responses; it emits a success event\n\t * when a retry was in progress.\n\t */\n\tonSuccessfulAssistantResponse(): void {\n\t\tif (this._attempt > 0) {\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: true,\n\t\t\t\tattempt: this._attempt,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t}\n\t}\n\n\t/** Resolve the pending retry promise */\n\tresolve(): void {\n\t\tif (this._resolve) {\n\t\t\tthis._resolve();\n\t\t\tthis._resolve = undefined;\n\t\t\tthis._promise = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Handle retryable errors with exponential backoff.\n\t * @returns true if retry was initiated, false if max retries exceeded or disabled\n\t */\n\tasync handleRetryableError(message: AssistantMessage): Promise<boolean> {\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\n\t\t// Retry promise is created synchronously in createPromiseForAgentEnd for agent_end.\n\t\t// Keep a defensive fallback here in case a future refactor bypasses that path.\n\t\tif (!this._promise) {\n\t\t\tthis._promise = new Promise((resolve) => {\n\t\t\t\tthis._resolve = resolve;\n\t\t\t});\n\t\t}\n\n\t\tthis._attempt++;\n\n\t\tif (this._attempt > settings.maxRetries) {\n\t\t\t// Max retries exceeded, emit final failure and reset\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt: this._attempt - 1,\n\t\t\t\tfinalError: message.errorMessage,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t\tthis.resolve(); // Resolve so waitForRetry() completes\n\t\t\treturn false;\n\t\t}\n\n\t\tconst delayMs = settings.baseDelayMs * 2 ** (this._attempt - 1);\n\n\t\tthis.deps.emit({\n\t\t\ttype: \"auto_retry_start\",\n\t\t\tattempt: this._attempt,\n\t\t\tmaxAttempts: settings.maxRetries,\n\t\t\tdelayMs,\n\t\t\terrorMessage: message.errorMessage || \"Unknown error\",\n\t\t});\n\n\t\t// Remove error message from agent state (keep in session for history)\n\t\tconst messages = this.deps.getAgentMessages();\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.deps.setAgentMessages(messages.slice(0, -1));\n\t\t}\n\n\t\t// Wait with exponential backoff (abortable)\n\t\tthis._abortController = new AbortController();\n\t\ttry {\n\t\t\tawait sleep(delayMs, this._abortController.signal);\n\t\t} catch {\n\t\t\t// Aborted during sleep - emit end event so UI can clean up\n\t\t\tconst attempt = this._attempt;\n\t\t\tthis._attempt = 0;\n\t\t\tthis._abortController = undefined;\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt,\n\t\t\t\tfinalError: \"Retry cancelled\",\n\t\t\t});\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\t\tthis._abortController = undefined;\n\n\t\t// Retry via continue() - use setTimeout to break out of event handler chain\n\t\tsetTimeout(() => {\n\t\t\tthis.deps.continueAgent();\n\t\t}, 0);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Cancel in-progress retry.\n\t */\n\tabort(): void {\n\t\tthis._abortController?.abort();\n\t\t// Note: _attempt is reset in the catch block of handleRetryableError\n\t\tthis.resolve();\n\t}\n\n\t/**\n\t * Wait for any in-progress retry to complete.\n\t * Returns immediately if no retry is in progress.\n\t */\n\tasync waitForRetry(): Promise<void> {\n\t\tif (!this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait this._promise;\n\t\tawait this.deps.waitForAgentIdle();\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"agent-session-retry.js","sourceRoot":"","sources":["../../src/core/agent-session-retry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Auto-retry controller for AgentSession.\n *\n * Owns the retry lifecycle for transient assistant errors (overloaded, rate\n * limit, server/network/transport failures): deciding whether an error is\n * retryable, arming the retry promise synchronously on agent_end, backing off\n * exponentially, and re-driving the agent via continue(). Context-overflow\n * errors are intentionally excluded here — those are handled by compaction.\n */\n\nimport type { AgentEvent, AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport { isContextOverflow, isLongRetryDelayError } from \"@kolisachint/hoocode-ai\";\nimport { sleep } from \"../utils/sleep.js\";\nimport type { AgentSessionEvent } from \"./agent-session.js\";\n\n/**\n * Retryable error signatures (overloaded, rate limit, server/network errors,\n * transport closes). Compiled once at module load instead of on every assistant\n * response. Context-overflow errors are handled separately by compaction.\n */\nconst RETRYABLE_ERROR_PATTERN =\n\t/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;\n\n/** Narrow dependencies the retry controller needs from AgentSession. */\nexport interface AutoRetryDeps {\n\tgetRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number };\n\tgetModel(): Model<any> | undefined;\n\tgetAgentMessages(): AgentMessage[];\n\tsetAgentMessages(messages: AgentMessage[]): void;\n\t/** Fire-and-forget continue() on the agent (errors surface on the next agent_end). */\n\tcontinueAgent(): void;\n\twaitForAgentIdle(): Promise<void>;\n\temit(event: AgentSessionEvent): void;\n}\n\nexport class AutoRetryController {\n\tprivate _abortController: AbortController | undefined = undefined;\n\tprivate _attempt = 0;\n\tprivate _promise: Promise<void> | undefined = undefined;\n\tprivate _resolve: (() => void) | undefined = undefined;\n\n\tconstructor(private readonly deps: AutoRetryDeps) {}\n\n\t/** Current retry attempt (0 if not retrying) */\n\tget attempt(): number {\n\t\treturn this._attempt;\n\t}\n\n\t/** Whether a retry is currently in progress */\n\tget isRetrying(): boolean {\n\t\treturn this._promise !== undefined;\n\t}\n\n\t/**\n\t * Check if an error is retryable (overloaded, rate limit, server errors).\n\t * Context overflow errors are NOT retryable (handled by compaction instead).\n\t */\n\tisRetryableError(message: AssistantMessage): boolean {\n\t\tif (message.stopReason !== \"error\" || !message.errorMessage) return false;\n\n\t\t// Context overflow is handled by compaction, not retry\n\t\tconst contextWindow = this.deps.getModel()?.contextWindow ?? 0;\n\t\tif (isContextOverflow(message, contextWindow)) return false;\n\n\t\t// An exhausted quota answers 429 the same way a burst rate limit does,\n\t\t// and the pattern below cannot tell them apart — both say \"429\". The\n\t\t// difference is in how long the provider asked us to wait: seconds mean\n\t\t// try again, weeks mean this session is not getting through no matter\n\t\t// how patiently it backs off. Retrying the second kind spends the whole\n\t\t// budget in a few seconds and reports failure as if the network were at\n\t\t// fault.\n\t\tif (isLongRetryDelayError(message.errorMessage)) return false;\n\n\t\treturn RETRYABLE_ERROR_PATTERN.test(message.errorMessage);\n\t}\n\n\t/**\n\t * Create the retry promise synchronously when an agent_end carries a\n\t * retryable error. Agent.emit() runs handlers synchronously and prompt()\n\t * calls waitForRetry() as soon as agent.prompt() resolves; arming the promise\n\t * here (rather than inside async event processing) ensures waitForRetry()\n\t * never misses an in-flight retry.\n\t */\n\tcreatePromiseForAgentEnd(event: AgentEvent): void {\n\t\tif (event.type !== \"agent_end\" || this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst lastAssistant = this._findLastAssistantInMessages(event.messages);\n\t\tif (!lastAssistant || !this.isRetryableError(lastAssistant)) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis._promise = new Promise((resolve) => {\n\t\t\tthis._resolve = resolve;\n\t\t});\n\t}\n\n\tprivate _findLastAssistantInMessages(messages: AgentMessage[]): AssistantMessage | undefined {\n\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\tconst message = messages[i];\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\treturn message as AssistantMessage;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Reset the attempt counter after a successful assistant response.\n\t * Callers invoke this only for non-error responses; it emits a success event\n\t * when a retry was in progress.\n\t */\n\tonSuccessfulAssistantResponse(): void {\n\t\tif (this._attempt > 0) {\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: true,\n\t\t\t\tattempt: this._attempt,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t}\n\t}\n\n\t/** Resolve the pending retry promise */\n\tresolve(): void {\n\t\tif (this._resolve) {\n\t\t\tthis._resolve();\n\t\t\tthis._resolve = undefined;\n\t\t\tthis._promise = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Handle retryable errors with exponential backoff.\n\t * @returns true if retry was initiated, false if max retries exceeded or disabled\n\t */\n\tasync handleRetryableError(message: AssistantMessage): Promise<boolean> {\n\t\tconst settings = this.deps.getRetrySettings();\n\t\tif (!settings.enabled) {\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\n\t\t// Retry promise is created synchronously in createPromiseForAgentEnd for agent_end.\n\t\t// Keep a defensive fallback here in case a future refactor bypasses that path.\n\t\tif (!this._promise) {\n\t\t\tthis._promise = new Promise((resolve) => {\n\t\t\t\tthis._resolve = resolve;\n\t\t\t});\n\t\t}\n\n\t\tthis._attempt++;\n\n\t\tif (this._attempt > settings.maxRetries) {\n\t\t\t// Max retries exceeded, emit final failure and reset\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt: this._attempt - 1,\n\t\t\t\tfinalError: message.errorMessage,\n\t\t\t});\n\t\t\tthis._attempt = 0;\n\t\t\tthis.resolve(); // Resolve so waitForRetry() completes\n\t\t\treturn false;\n\t\t}\n\n\t\tconst delayMs = settings.baseDelayMs * 2 ** (this._attempt - 1);\n\n\t\tthis.deps.emit({\n\t\t\ttype: \"auto_retry_start\",\n\t\t\tattempt: this._attempt,\n\t\t\tmaxAttempts: settings.maxRetries,\n\t\t\tdelayMs,\n\t\t\terrorMessage: message.errorMessage || \"Unknown error\",\n\t\t});\n\n\t\t// Remove error message from agent state (keep in session for history)\n\t\tconst messages = this.deps.getAgentMessages();\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.deps.setAgentMessages(messages.slice(0, -1));\n\t\t}\n\n\t\t// Wait with exponential backoff (abortable)\n\t\tthis._abortController = new AbortController();\n\t\ttry {\n\t\t\tawait sleep(delayMs, this._abortController.signal);\n\t\t} catch {\n\t\t\t// Aborted during sleep - emit end event so UI can clean up\n\t\t\tconst attempt = this._attempt;\n\t\t\tthis._attempt = 0;\n\t\t\tthis._abortController = undefined;\n\t\t\tthis.deps.emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt,\n\t\t\t\tfinalError: \"Retry cancelled\",\n\t\t\t});\n\t\t\tthis.resolve();\n\t\t\treturn false;\n\t\t}\n\t\tthis._abortController = undefined;\n\n\t\t// Retry via continue() - use setTimeout to break out of event handler chain\n\t\tsetTimeout(() => {\n\t\t\tthis.deps.continueAgent();\n\t\t}, 0);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Cancel in-progress retry.\n\t */\n\tabort(): void {\n\t\tthis._abortController?.abort();\n\t\t// Note: _attempt is reset in the catch block of handleRetryableError\n\t\tthis.resolve();\n\t}\n\n\t/**\n\t * Wait for any in-progress retry to complete.\n\t * Returns immediately if no retry is in progress.\n\t */\n\tasync waitForRetry(): Promise<void> {\n\t\tif (!this._promise) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait this._promise;\n\t\tawait this.deps.waitForAgentIdle();\n\t}\n}\n"]}
|
|
@@ -22,4 +22,14 @@ export declare function sessionSlugFor(sessionId: string): string;
|
|
|
22
22
|
export declare function sessionColorSlotFor(sessionId: string): number;
|
|
23
23
|
/** Whether `slot` is one a session can actually be assigned. */
|
|
24
24
|
export declare function isSessionColorSlot(slot: number): boolean;
|
|
25
|
+
/** The canonical slot names, in slot order. For usage lines and pickers. */
|
|
26
|
+
export declare const SESSION_COLOR_NAME_LIST: readonly string[];
|
|
27
|
+
/** What to call `slot` when reporting it back, or undefined if it is not a slot. */
|
|
28
|
+
export declare function sessionColorName(slot: number): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The slot an argument to `/color` asks for: a number (`4`), a name
|
|
31
|
+
* (`magenta`), or an initial (`m`). Case and surrounding space do not matter.
|
|
32
|
+
* Undefined when it names no slot at all.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseSessionColorSlot(input: string): number | undefined;
|
|
25
35
|
//# sourceMappingURL=session-identity.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-identity.d.ts","sourceRoot":"","sources":["../../src/core/session-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,0FAA0F;AAC1F,eAAO,MAAM,mBAAmB,IAAI,CAAC;AA8GrC,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,iFAA+E;AAC/E,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,gEAAgE;AAChE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD","sourcesContent":["/**\n * A session's visual identity: the name it carries before anyone names it, and\n * the palette slot its chip is filled with.\n *\n * Both are pure functions of the session id, which means they need no storage of\n * their own: the id is already in the session header, so a resumed session comes\n * back wearing the same slug and the same colour, and a fork — which mints a new\n * id — gets its own identity instead of quietly wearing its parent's. What *is*\n * stored is an override: `/name` and `/color` write to the session's\n * `session_info` entry, and those win over everything here (see\n * SessionManager.getDisplayName / getSessionColorSlot).\n *\n * This module is deliberately theme-free. It deals in slot *numbers*; mapping a\n * slot onto a colour token is the theme's job, so `core/` never has to reach into\n * a mode's rendering code.\n */\n\n/** Number of colour slots a session can land in. Matches the theme's identity palette. */\nexport const SESSION_COLOR_SLOTS = 6;\n\n/**\n * The slug vocabulary. Chosen for the terminal, not for poetry: every word is\n * short (so a two-word slug stays well inside the chip's 20-column cap), lower\n * case, unambiguous when read at a glance from across a desk, and free of the\n * near-homographs that make two sessions hard to tell apart (no \"lake\"/\"lane\").\n * 32 × 32 gives 1024 combinations, which is far more than the number of sessions\n * anyone has open at once — the point is recognition, not uniqueness.\n */\nconst ADJECTIVES = [\n\t\"amber\",\n\t\"ash\",\n\t\"azure\",\n\t\"brisk\",\n\t\"bronze\",\n\t\"calm\",\n\t\"clay\",\n\t\"coral\",\n\t\"crisp\",\n\t\"dusk\",\n\t\"ember\",\n\t\"fern\",\n\t\"flint\",\n\t\"frost\",\n\t\"gold\",\n\t\"ivory\",\n\t\"jade\",\n\t\"lunar\",\n\t\"mellow\",\n\t\"mint\",\n\t\"olive\",\n\t\"onyx\",\n\t\"plum\",\n\t\"quiet\",\n\t\"rust\",\n\t\"sable\",\n\t\"sage\",\n\t\"slate\",\n\t\"solar\",\n\t\"swift\",\n\t\"teal\",\n\t\"velvet\",\n] as const;\n\nconst NOUNS = [\n\t\"anchor\",\n\t\"arbor\",\n\t\"basin\",\n\t\"beacon\",\n\t\"birch\",\n\t\"canyon\",\n\t\"cedar\",\n\t\"cinder\",\n\t\"cove\",\n\t\"delta\",\n\t\"drift\",\n\t\"ferry\",\n\t\"forge\",\n\t\"harbor\",\n\t\"hollow\",\n\t\"lantern\",\n\t\"meadow\",\n\t\"mesa\",\n\t\"orchard\",\n\t\"pier\",\n\t\"prairie\",\n\t\"quarry\",\n\t\"ridge\",\n\t\"summit\",\n\t\"thicket\",\n\t\"tide\",\n\t\"trellis\",\n\t\"vale\",\n\t\"willow\",\n\t\"windmill\",\n\t\"yarrow\",\n\t\"zenith\",\n] as const;\n\n/**\n * djb2 over the whole string. The same hash the agent palette uses — tiny,\n * stable across runs and machines, and good spread for identifier-like input.\n *\n * Hashing the *whole* id matters here. Session ids are uuidv7, which is\n * time-ordered: two sessions started in the same minute share their leading\n * characters, so a hash over a prefix would drop them into the same slug and the\n * same colour — exactly the sessions a person most needs to tell apart.\n */\nfunction djb2(value: string): number {\n\tlet hash = 5381;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0;\n\t}\n\treturn hash;\n}\n\n/**\n * A second hash over the same string, seeded differently (sdbm). The slug needs\n * two independent indices; running djb2 twice would tie the noun to the\n * adjective and collapse the vocabulary from 1024 pairs to 32.\n */\nfunction sdbm(value: string): number {\n\tlet hash = 0;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = (value.charCodeAt(i) + (hash << 6) + (hash << 16) - hash) >>> 0;\n\t}\n\treturn hash;\n}\n\n/** The auto-assigned name for a session: a memorable two-word slug, e.g. `amber-harbor`. */\nexport function sessionSlugFor(sessionId: string): string {\n\tconst adjective = ADJECTIVES[djb2(sessionId) % ADJECTIVES.length];\n\tconst noun = NOUNS[sdbm(sessionId) % NOUNS.length];\n\treturn `${adjective}-${noun}`;\n}\n\n/** The auto-assigned colour slot for a session, in `1…SESSION_COLOR_SLOTS`. */\nexport function sessionColorSlotFor(sessionId: string): number {\n\treturn (djb2(sessionId) % SESSION_COLOR_SLOTS) + 1;\n}\n\n/** Whether `slot` is one a session can actually be assigned. */\nexport function isSessionColorSlot(slot: number): boolean {\n\treturn Number.isInteger(slot) && slot >= 1 && slot <= SESSION_COLOR_SLOTS;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-identity.d.ts","sourceRoot":"","sources":["../../src/core/session-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,0FAA0F;AAC1F,eAAO,MAAM,mBAAmB,IAAI,CAAC;AA8GrC,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,iFAA+E;AAC/E,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,gEAAgE;AAChE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAiCD,4EAA4E;AAC5E,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAAmD,CAAC;AAEzG,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASvE","sourcesContent":["/**\n * A session's visual identity: the name it carries before anyone names it, and\n * the palette slot its chip is filled with.\n *\n * Both are pure functions of the session id, which means they need no storage of\n * their own: the id is already in the session header, so a resumed session comes\n * back wearing the same slug and the same colour, and a fork — which mints a new\n * id — gets its own identity instead of quietly wearing its parent's. What *is*\n * stored is an override: `/name` and `/color` write to the session's\n * `session_info` entry, and those win over everything here (see\n * SessionManager.getDisplayName / getSessionColorSlot).\n *\n * This module is deliberately theme-free. It deals in slot *numbers*; mapping a\n * slot onto a colour token is the theme's job, so `core/` never has to reach into\n * a mode's rendering code.\n */\n\n/** Number of colour slots a session can land in. Matches the theme's identity palette. */\nexport const SESSION_COLOR_SLOTS = 6;\n\n/**\n * The slug vocabulary. Chosen for the terminal, not for poetry: every word is\n * short (so a two-word slug stays well inside the chip's 20-column cap), lower\n * case, unambiguous when read at a glance from across a desk, and free of the\n * near-homographs that make two sessions hard to tell apart (no \"lake\"/\"lane\").\n * 32 × 32 gives 1024 combinations, which is far more than the number of sessions\n * anyone has open at once — the point is recognition, not uniqueness.\n */\nconst ADJECTIVES = [\n\t\"amber\",\n\t\"ash\",\n\t\"azure\",\n\t\"brisk\",\n\t\"bronze\",\n\t\"calm\",\n\t\"clay\",\n\t\"coral\",\n\t\"crisp\",\n\t\"dusk\",\n\t\"ember\",\n\t\"fern\",\n\t\"flint\",\n\t\"frost\",\n\t\"gold\",\n\t\"ivory\",\n\t\"jade\",\n\t\"lunar\",\n\t\"mellow\",\n\t\"mint\",\n\t\"olive\",\n\t\"onyx\",\n\t\"plum\",\n\t\"quiet\",\n\t\"rust\",\n\t\"sable\",\n\t\"sage\",\n\t\"slate\",\n\t\"solar\",\n\t\"swift\",\n\t\"teal\",\n\t\"velvet\",\n] as const;\n\nconst NOUNS = [\n\t\"anchor\",\n\t\"arbor\",\n\t\"basin\",\n\t\"beacon\",\n\t\"birch\",\n\t\"canyon\",\n\t\"cedar\",\n\t\"cinder\",\n\t\"cove\",\n\t\"delta\",\n\t\"drift\",\n\t\"ferry\",\n\t\"forge\",\n\t\"harbor\",\n\t\"hollow\",\n\t\"lantern\",\n\t\"meadow\",\n\t\"mesa\",\n\t\"orchard\",\n\t\"pier\",\n\t\"prairie\",\n\t\"quarry\",\n\t\"ridge\",\n\t\"summit\",\n\t\"thicket\",\n\t\"tide\",\n\t\"trellis\",\n\t\"vale\",\n\t\"willow\",\n\t\"windmill\",\n\t\"yarrow\",\n\t\"zenith\",\n] as const;\n\n/**\n * djb2 over the whole string. The same hash the agent palette uses — tiny,\n * stable across runs and machines, and good spread for identifier-like input.\n *\n * Hashing the *whole* id matters here. Session ids are uuidv7, which is\n * time-ordered: two sessions started in the same minute share their leading\n * characters, so a hash over a prefix would drop them into the same slug and the\n * same colour — exactly the sessions a person most needs to tell apart.\n */\nfunction djb2(value: string): number {\n\tlet hash = 5381;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0;\n\t}\n\treturn hash;\n}\n\n/**\n * A second hash over the same string, seeded differently (sdbm). The slug needs\n * two independent indices; running djb2 twice would tie the noun to the\n * adjective and collapse the vocabulary from 1024 pairs to 32.\n */\nfunction sdbm(value: string): number {\n\tlet hash = 0;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = (value.charCodeAt(i) + (hash << 6) + (hash << 16) - hash) >>> 0;\n\t}\n\treturn hash;\n}\n\n/** The auto-assigned name for a session: a memorable two-word slug, e.g. `amber-harbor`. */\nexport function sessionSlugFor(sessionId: string): string {\n\tconst adjective = ADJECTIVES[djb2(sessionId) % ADJECTIVES.length];\n\tconst noun = NOUNS[sdbm(sessionId) % NOUNS.length];\n\treturn `${adjective}-${noun}`;\n}\n\n/** The auto-assigned colour slot for a session, in `1…SESSION_COLOR_SLOTS`. */\nexport function sessionColorSlotFor(sessionId: string): number {\n\treturn (djb2(sessionId) % SESSION_COLOR_SLOTS) + 1;\n}\n\n/** Whether `slot` is one a session can actually be assigned. */\nexport function isSessionColorSlot(slot: number): boolean {\n\treturn Number.isInteger(slot) && slot >= 1 && slot <= SESSION_COLOR_SLOTS;\n}\n\n/**\n * The name each slot answers to, and the other spellings that reach it.\n *\n * A slot is a palette position, not a fixed hue — every theme fills\n * `agent1…agent6` with its own colours — but across the built-in themes each\n * position keeps a recognisable family, and that is what these names describe:\n * slot 3 is the warm yellow/amber one wherever you are, slot 6 the blue. That\n * is enough for the thing names are for, which is typing `/color green`\n * instead of remembering that green is the fifth swatch.\n *\n * The aliases are deliberately generous, because a name only helps if the\n * obvious spelling of it works. Each name's initial stands for it, and the\n * hues that sit between slots (`orange`, `red`) resolve to the nearest slot\n * rather than being rejected — the palette has six entries, not a full colour\n * wheel, and refusing `/color red` because the swatch is officially magenta\n * would be pedantry. A theme whose palette drifts from these names is still\n * addressable by number, which is why the numbers never go away.\n */\nconst SESSION_COLOR_NAMES: ReadonlyArray<{\n\treadonly slot: number;\n\treadonly name: string;\n\treadonly aliases: readonly string[];\n}> = [\n\t{ slot: 1, name: \"cyan\", aliases: [\"c\", \"teal\", \"aqua\"] },\n\t{ slot: 2, name: \"purple\", aliases: [\"p\", \"violet\"] },\n\t{ slot: 3, name: \"yellow\", aliases: [\"y\", \"amber\", \"gold\", \"orange\", \"o\"] },\n\t{ slot: 4, name: \"magenta\", aliases: [\"m\", \"pink\", \"rose\", \"red\", \"r\"] },\n\t{ slot: 5, name: \"green\", aliases: [\"g\"] },\n\t{ slot: 6, name: \"blue\", aliases: [\"b\"] },\n];\n\n/** The canonical slot names, in slot order. For usage lines and pickers. */\nexport const SESSION_COLOR_NAME_LIST: readonly string[] = SESSION_COLOR_NAMES.map((entry) => entry.name);\n\n/** What to call `slot` when reporting it back, or undefined if it is not a slot. */\nexport function sessionColorName(slot: number): string | undefined {\n\treturn SESSION_COLOR_NAMES.find((entry) => entry.slot === slot)?.name;\n}\n\n/**\n * The slot an argument to `/color` asks for: a number (`4`), a name\n * (`magenta`), or an initial (`m`). Case and surrounding space do not matter.\n * Undefined when it names no slot at all.\n */\nexport function parseSessionColorSlot(input: string): number | undefined {\n\tconst normalized = input.trim().toLowerCase();\n\tif (!normalized) return undefined;\n\tif (/^\\d+$/.test(normalized)) {\n\t\tconst slot = Number(normalized);\n\t\treturn isSessionColorSlot(slot) ? slot : undefined;\n\t}\n\tconst match = SESSION_COLOR_NAMES.find((entry) => entry.name === normalized || entry.aliases.includes(normalized));\n\treturn match?.slot;\n}\n"]}
|
|
@@ -134,4 +134,52 @@ export function sessionColorSlotFor(sessionId) {
|
|
|
134
134
|
export function isSessionColorSlot(slot) {
|
|
135
135
|
return Number.isInteger(slot) && slot >= 1 && slot <= SESSION_COLOR_SLOTS;
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The name each slot answers to, and the other spellings that reach it.
|
|
139
|
+
*
|
|
140
|
+
* A slot is a palette position, not a fixed hue — every theme fills
|
|
141
|
+
* `agent1…agent6` with its own colours — but across the built-in themes each
|
|
142
|
+
* position keeps a recognisable family, and that is what these names describe:
|
|
143
|
+
* slot 3 is the warm yellow/amber one wherever you are, slot 6 the blue. That
|
|
144
|
+
* is enough for the thing names are for, which is typing `/color green`
|
|
145
|
+
* instead of remembering that green is the fifth swatch.
|
|
146
|
+
*
|
|
147
|
+
* The aliases are deliberately generous, because a name only helps if the
|
|
148
|
+
* obvious spelling of it works. Each name's initial stands for it, and the
|
|
149
|
+
* hues that sit between slots (`orange`, `red`) resolve to the nearest slot
|
|
150
|
+
* rather than being rejected — the palette has six entries, not a full colour
|
|
151
|
+
* wheel, and refusing `/color red` because the swatch is officially magenta
|
|
152
|
+
* would be pedantry. A theme whose palette drifts from these names is still
|
|
153
|
+
* addressable by number, which is why the numbers never go away.
|
|
154
|
+
*/
|
|
155
|
+
const SESSION_COLOR_NAMES = [
|
|
156
|
+
{ slot: 1, name: "cyan", aliases: ["c", "teal", "aqua"] },
|
|
157
|
+
{ slot: 2, name: "purple", aliases: ["p", "violet"] },
|
|
158
|
+
{ slot: 3, name: "yellow", aliases: ["y", "amber", "gold", "orange", "o"] },
|
|
159
|
+
{ slot: 4, name: "magenta", aliases: ["m", "pink", "rose", "red", "r"] },
|
|
160
|
+
{ slot: 5, name: "green", aliases: ["g"] },
|
|
161
|
+
{ slot: 6, name: "blue", aliases: ["b"] },
|
|
162
|
+
];
|
|
163
|
+
/** The canonical slot names, in slot order. For usage lines and pickers. */
|
|
164
|
+
export const SESSION_COLOR_NAME_LIST = SESSION_COLOR_NAMES.map((entry) => entry.name);
|
|
165
|
+
/** What to call `slot` when reporting it back, or undefined if it is not a slot. */
|
|
166
|
+
export function sessionColorName(slot) {
|
|
167
|
+
return SESSION_COLOR_NAMES.find((entry) => entry.slot === slot)?.name;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The slot an argument to `/color` asks for: a number (`4`), a name
|
|
171
|
+
* (`magenta`), or an initial (`m`). Case and surrounding space do not matter.
|
|
172
|
+
* Undefined when it names no slot at all.
|
|
173
|
+
*/
|
|
174
|
+
export function parseSessionColorSlot(input) {
|
|
175
|
+
const normalized = input.trim().toLowerCase();
|
|
176
|
+
if (!normalized)
|
|
177
|
+
return undefined;
|
|
178
|
+
if (/^\d+$/.test(normalized)) {
|
|
179
|
+
const slot = Number(normalized);
|
|
180
|
+
return isSessionColorSlot(slot) ? slot : undefined;
|
|
181
|
+
}
|
|
182
|
+
const match = SESSION_COLOR_NAMES.find((entry) => entry.name === normalized || entry.aliases.includes(normalized));
|
|
183
|
+
return match?.slot;
|
|
184
|
+
}
|
|
137
185
|
//# sourceMappingURL=session-identity.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-identity.js","sourceRoot":"","sources":["../../src/core/session-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,0FAA0F;AAC1F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAErC;;;;;;;GAOG;AACH,MAAM,UAAU,GAAG;IAClB,OAAO;IACP,KAAK;IACL,OAAO;IACP,OAAO;IACP,QAAQ;IACR,MAAM;IACN,MAAM;IACN,OAAO;IACP,OAAO;IACP,MAAM;IACN,OAAO;IACP,MAAM;IACN,OAAO;IACP,OAAO;IACP,MAAM;IACN,OAAO;IACP,MAAM;IACN,OAAO;IACP,QAAQ;IACR,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,OAAO;IACP,MAAM;IACN,OAAO;IACP,MAAM;IACN,OAAO;IACP,OAAO;IACP,OAAO;IACP,MAAM;IACN,QAAQ;CACC,CAAC;AAEX,MAAM,KAAK,GAAG;IACb,QAAQ;IACR,OAAO;IACP,OAAO;IACP,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,MAAM;IACN,OAAO;IACP,OAAO;IACP,OAAO;IACP,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,MAAM;IACN,SAAS;IACT,MAAM;IACN,SAAS;IACT,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,SAAS;IACT,MAAM;IACN,SAAS;IACT,MAAM;IACN,QAAQ;IACR,UAAU;IACV,QAAQ;IACR,QAAQ;CACC,CAAC;AAEX;;;;;;;;GAQG;AACH,SAAS,IAAI,CAAC,KAAa,EAAU;IACpC,IAAI,IAAI,GAAG,IAAI,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED;;;;GAIG;AACH,SAAS,IAAI,CAAC,KAAa,EAAU;IACpC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,4FAA4F;AAC5F,MAAM,UAAU,cAAc,CAAC,SAAiB,EAAU;IACzD,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAClE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACnD,OAAO,GAAG,SAAS,IAAI,IAAI,EAAE,CAAC;AAAA,CAC9B;AAED,iFAA+E;AAC/E,MAAM,UAAU,mBAAmB,CAAC,SAAiB,EAAU;IAC9D,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;AAAA,CACnD;AAED,gEAAgE;AAChE,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAW;IACzD,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,mBAAmB,CAAC;AAAA,CAC1E","sourcesContent":["/**\n * A session's visual identity: the name it carries before anyone names it, and\n * the palette slot its chip is filled with.\n *\n * Both are pure functions of the session id, which means they need no storage of\n * their own: the id is already in the session header, so a resumed session comes\n * back wearing the same slug and the same colour, and a fork — which mints a new\n * id — gets its own identity instead of quietly wearing its parent's. What *is*\n * stored is an override: `/name` and `/color` write to the session's\n * `session_info` entry, and those win over everything here (see\n * SessionManager.getDisplayName / getSessionColorSlot).\n *\n * This module is deliberately theme-free. It deals in slot *numbers*; mapping a\n * slot onto a colour token is the theme's job, so `core/` never has to reach into\n * a mode's rendering code.\n */\n\n/** Number of colour slots a session can land in. Matches the theme's identity palette. */\nexport const SESSION_COLOR_SLOTS = 6;\n\n/**\n * The slug vocabulary. Chosen for the terminal, not for poetry: every word is\n * short (so a two-word slug stays well inside the chip's 20-column cap), lower\n * case, unambiguous when read at a glance from across a desk, and free of the\n * near-homographs that make two sessions hard to tell apart (no \"lake\"/\"lane\").\n * 32 × 32 gives 1024 combinations, which is far more than the number of sessions\n * anyone has open at once — the point is recognition, not uniqueness.\n */\nconst ADJECTIVES = [\n\t\"amber\",\n\t\"ash\",\n\t\"azure\",\n\t\"brisk\",\n\t\"bronze\",\n\t\"calm\",\n\t\"clay\",\n\t\"coral\",\n\t\"crisp\",\n\t\"dusk\",\n\t\"ember\",\n\t\"fern\",\n\t\"flint\",\n\t\"frost\",\n\t\"gold\",\n\t\"ivory\",\n\t\"jade\",\n\t\"lunar\",\n\t\"mellow\",\n\t\"mint\",\n\t\"olive\",\n\t\"onyx\",\n\t\"plum\",\n\t\"quiet\",\n\t\"rust\",\n\t\"sable\",\n\t\"sage\",\n\t\"slate\",\n\t\"solar\",\n\t\"swift\",\n\t\"teal\",\n\t\"velvet\",\n] as const;\n\nconst NOUNS = [\n\t\"anchor\",\n\t\"arbor\",\n\t\"basin\",\n\t\"beacon\",\n\t\"birch\",\n\t\"canyon\",\n\t\"cedar\",\n\t\"cinder\",\n\t\"cove\",\n\t\"delta\",\n\t\"drift\",\n\t\"ferry\",\n\t\"forge\",\n\t\"harbor\",\n\t\"hollow\",\n\t\"lantern\",\n\t\"meadow\",\n\t\"mesa\",\n\t\"orchard\",\n\t\"pier\",\n\t\"prairie\",\n\t\"quarry\",\n\t\"ridge\",\n\t\"summit\",\n\t\"thicket\",\n\t\"tide\",\n\t\"trellis\",\n\t\"vale\",\n\t\"willow\",\n\t\"windmill\",\n\t\"yarrow\",\n\t\"zenith\",\n] as const;\n\n/**\n * djb2 over the whole string. The same hash the agent palette uses — tiny,\n * stable across runs and machines, and good spread for identifier-like input.\n *\n * Hashing the *whole* id matters here. Session ids are uuidv7, which is\n * time-ordered: two sessions started in the same minute share their leading\n * characters, so a hash over a prefix would drop them into the same slug and the\n * same colour — exactly the sessions a person most needs to tell apart.\n */\nfunction djb2(value: string): number {\n\tlet hash = 5381;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0;\n\t}\n\treturn hash;\n}\n\n/**\n * A second hash over the same string, seeded differently (sdbm). The slug needs\n * two independent indices; running djb2 twice would tie the noun to the\n * adjective and collapse the vocabulary from 1024 pairs to 32.\n */\nfunction sdbm(value: string): number {\n\tlet hash = 0;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = (value.charCodeAt(i) + (hash << 6) + (hash << 16) - hash) >>> 0;\n\t}\n\treturn hash;\n}\n\n/** The auto-assigned name for a session: a memorable two-word slug, e.g. `amber-harbor`. */\nexport function sessionSlugFor(sessionId: string): string {\n\tconst adjective = ADJECTIVES[djb2(sessionId) % ADJECTIVES.length];\n\tconst noun = NOUNS[sdbm(sessionId) % NOUNS.length];\n\treturn `${adjective}-${noun}`;\n}\n\n/** The auto-assigned colour slot for a session, in `1…SESSION_COLOR_SLOTS`. */\nexport function sessionColorSlotFor(sessionId: string): number {\n\treturn (djb2(sessionId) % SESSION_COLOR_SLOTS) + 1;\n}\n\n/** Whether `slot` is one a session can actually be assigned. */\nexport function isSessionColorSlot(slot: number): boolean {\n\treturn Number.isInteger(slot) && slot >= 1 && slot <= SESSION_COLOR_SLOTS;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-identity.js","sourceRoot":"","sources":["../../src/core/session-identity.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A session's visual identity: the name it carries before anyone names it, and\n * the palette slot its chip is filled with.\n *\n * Both are pure functions of the session id, which means they need no storage of\n * their own: the id is already in the session header, so a resumed session comes\n * back wearing the same slug and the same colour, and a fork — which mints a new\n * id — gets its own identity instead of quietly wearing its parent's. What *is*\n * stored is an override: `/name` and `/color` write to the session's\n * `session_info` entry, and those win over everything here (see\n * SessionManager.getDisplayName / getSessionColorSlot).\n *\n * This module is deliberately theme-free. It deals in slot *numbers*; mapping a\n * slot onto a colour token is the theme's job, so `core/` never has to reach into\n * a mode's rendering code.\n */\n\n/** Number of colour slots a session can land in. Matches the theme's identity palette. */\nexport const SESSION_COLOR_SLOTS = 6;\n\n/**\n * The slug vocabulary. Chosen for the terminal, not for poetry: every word is\n * short (so a two-word slug stays well inside the chip's 20-column cap), lower\n * case, unambiguous when read at a glance from across a desk, and free of the\n * near-homographs that make two sessions hard to tell apart (no \"lake\"/\"lane\").\n * 32 × 32 gives 1024 combinations, which is far more than the number of sessions\n * anyone has open at once — the point is recognition, not uniqueness.\n */\nconst ADJECTIVES = [\n\t\"amber\",\n\t\"ash\",\n\t\"azure\",\n\t\"brisk\",\n\t\"bronze\",\n\t\"calm\",\n\t\"clay\",\n\t\"coral\",\n\t\"crisp\",\n\t\"dusk\",\n\t\"ember\",\n\t\"fern\",\n\t\"flint\",\n\t\"frost\",\n\t\"gold\",\n\t\"ivory\",\n\t\"jade\",\n\t\"lunar\",\n\t\"mellow\",\n\t\"mint\",\n\t\"olive\",\n\t\"onyx\",\n\t\"plum\",\n\t\"quiet\",\n\t\"rust\",\n\t\"sable\",\n\t\"sage\",\n\t\"slate\",\n\t\"solar\",\n\t\"swift\",\n\t\"teal\",\n\t\"velvet\",\n] as const;\n\nconst NOUNS = [\n\t\"anchor\",\n\t\"arbor\",\n\t\"basin\",\n\t\"beacon\",\n\t\"birch\",\n\t\"canyon\",\n\t\"cedar\",\n\t\"cinder\",\n\t\"cove\",\n\t\"delta\",\n\t\"drift\",\n\t\"ferry\",\n\t\"forge\",\n\t\"harbor\",\n\t\"hollow\",\n\t\"lantern\",\n\t\"meadow\",\n\t\"mesa\",\n\t\"orchard\",\n\t\"pier\",\n\t\"prairie\",\n\t\"quarry\",\n\t\"ridge\",\n\t\"summit\",\n\t\"thicket\",\n\t\"tide\",\n\t\"trellis\",\n\t\"vale\",\n\t\"willow\",\n\t\"windmill\",\n\t\"yarrow\",\n\t\"zenith\",\n] as const;\n\n/**\n * djb2 over the whole string. The same hash the agent palette uses — tiny,\n * stable across runs and machines, and good spread for identifier-like input.\n *\n * Hashing the *whole* id matters here. Session ids are uuidv7, which is\n * time-ordered: two sessions started in the same minute share their leading\n * characters, so a hash over a prefix would drop them into the same slug and the\n * same colour — exactly the sessions a person most needs to tell apart.\n */\nfunction djb2(value: string): number {\n\tlet hash = 5381;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = ((hash << 5) + hash + value.charCodeAt(i)) >>> 0;\n\t}\n\treturn hash;\n}\n\n/**\n * A second hash over the same string, seeded differently (sdbm). The slug needs\n * two independent indices; running djb2 twice would tie the noun to the\n * adjective and collapse the vocabulary from 1024 pairs to 32.\n */\nfunction sdbm(value: string): number {\n\tlet hash = 0;\n\tfor (let i = 0; i < value.length; i++) {\n\t\thash = (value.charCodeAt(i) + (hash << 6) + (hash << 16) - hash) >>> 0;\n\t}\n\treturn hash;\n}\n\n/** The auto-assigned name for a session: a memorable two-word slug, e.g. `amber-harbor`. */\nexport function sessionSlugFor(sessionId: string): string {\n\tconst adjective = ADJECTIVES[djb2(sessionId) % ADJECTIVES.length];\n\tconst noun = NOUNS[sdbm(sessionId) % NOUNS.length];\n\treturn `${adjective}-${noun}`;\n}\n\n/** The auto-assigned colour slot for a session, in `1…SESSION_COLOR_SLOTS`. */\nexport function sessionColorSlotFor(sessionId: string): number {\n\treturn (djb2(sessionId) % SESSION_COLOR_SLOTS) + 1;\n}\n\n/** Whether `slot` is one a session can actually be assigned. */\nexport function isSessionColorSlot(slot: number): boolean {\n\treturn Number.isInteger(slot) && slot >= 1 && slot <= SESSION_COLOR_SLOTS;\n}\n\n/**\n * The name each slot answers to, and the other spellings that reach it.\n *\n * A slot is a palette position, not a fixed hue — every theme fills\n * `agent1…agent6` with its own colours — but across the built-in themes each\n * position keeps a recognisable family, and that is what these names describe:\n * slot 3 is the warm yellow/amber one wherever you are, slot 6 the blue. That\n * is enough for the thing names are for, which is typing `/color green`\n * instead of remembering that green is the fifth swatch.\n *\n * The aliases are deliberately generous, because a name only helps if the\n * obvious spelling of it works. Each name's initial stands for it, and the\n * hues that sit between slots (`orange`, `red`) resolve to the nearest slot\n * rather than being rejected — the palette has six entries, not a full colour\n * wheel, and refusing `/color red` because the swatch is officially magenta\n * would be pedantry. A theme whose palette drifts from these names is still\n * addressable by number, which is why the numbers never go away.\n */\nconst SESSION_COLOR_NAMES: ReadonlyArray<{\n\treadonly slot: number;\n\treadonly name: string;\n\treadonly aliases: readonly string[];\n}> = [\n\t{ slot: 1, name: \"cyan\", aliases: [\"c\", \"teal\", \"aqua\"] },\n\t{ slot: 2, name: \"purple\", aliases: [\"p\", \"violet\"] },\n\t{ slot: 3, name: \"yellow\", aliases: [\"y\", \"amber\", \"gold\", \"orange\", \"o\"] },\n\t{ slot: 4, name: \"magenta\", aliases: [\"m\", \"pink\", \"rose\", \"red\", \"r\"] },\n\t{ slot: 5, name: \"green\", aliases: [\"g\"] },\n\t{ slot: 6, name: \"blue\", aliases: [\"b\"] },\n];\n\n/** The canonical slot names, in slot order. For usage lines and pickers. */\nexport const SESSION_COLOR_NAME_LIST: readonly string[] = SESSION_COLOR_NAMES.map((entry) => entry.name);\n\n/** What to call `slot` when reporting it back, or undefined if it is not a slot. */\nexport function sessionColorName(slot: number): string | undefined {\n\treturn SESSION_COLOR_NAMES.find((entry) => entry.slot === slot)?.name;\n}\n\n/**\n * The slot an argument to `/color` asks for: a number (`4`), a name\n * (`magenta`), or an initial (`m`). Case and surrounding space do not matter.\n * Undefined when it names no slot at all.\n */\nexport function parseSessionColorSlot(input: string): number | undefined {\n\tconst normalized = input.trim().toLowerCase();\n\tif (!normalized) return undefined;\n\tif (/^\\d+$/.test(normalized)) {\n\t\tconst slot = Number(normalized);\n\t\treturn isSessionColorSlot(slot) ? slot : undefined;\n\t}\n\tconst match = SESSION_COLOR_NAMES.find((entry) => entry.name === normalized || entry.aliases.includes(normalized));\n\treturn match?.slot;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,CA4BrE,CAAC","sourcesContent":["import { APP_NAME } from \"../config.js\";\nimport type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for model cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"color\", description: \"Set the session chip color
|
|
1
|
+
{"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,CA4BrE,CAAC","sourcesContent":["import { APP_NAME } from \"../config.js\";\nimport type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for model cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"color\", description: \"Set the session chip color: /color <1-6|name> (bare = pick one)\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{\n\t\tname: \"cd\",\n\t\tdescription: \"Change working directory: /cd <path> (bare = home, - = previous). Starts a session there.\",\n\t},\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n\t{ name: \"subagent\", description: \"Spawn a subagent directly: /subagent <mode> <task>\" },\n];\n"]}
|
|
@@ -8,7 +8,7 @@ export const BUILTIN_SLASH_COMMANDS = [
|
|
|
8
8
|
{ name: "share", description: "Share session as a secret GitHub gist" },
|
|
9
9
|
{ name: "copy", description: "Copy last agent message to clipboard" },
|
|
10
10
|
{ name: "name", description: "Set session display name" },
|
|
11
|
-
{ name: "color", description: "Set the session chip color
|
|
11
|
+
{ name: "color", description: "Set the session chip color: /color <1-6|name> (bare = pick one)" },
|
|
12
12
|
{ name: "session", description: "Show session info and stats" },
|
|
13
13
|
{ name: "changelog", description: "Show changelog entries" },
|
|
14
14
|
{ name: "hotkeys", description: "Show all keyboard shortcuts" },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiBxC,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE;IAClE,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACjF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiBxC,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE;IAClE,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACjF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,iEAAiE,EAAE;IACjG,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE;IAC5D,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gDAAgD,EAAE;IAC/E,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uDAAuD,EAAE;IACvF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACxE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,mCAAmC,EAAE;IACnE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gCAAgC,EAAE;IACjE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE;IACnD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACxE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC7D;QACC,IAAI,EAAE,IAAI;QACV,WAAW,EAAE,2FAA2F;KACxG;IACD,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,6DAA6D,EAAE;IAC9F,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,QAAQ,EAAE,EAAE;IACjD,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oDAAoD,EAAE;CACvF,CAAC","sourcesContent":["import { APP_NAME } from \"../config.js\";\nimport type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for model cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"color\", description: \"Set the session chip color: /color <1-6|name> (bare = pick one)\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{\n\t\tname: \"cd\",\n\t\tdescription: \"Change working directory: /cd <path> (bare = home, - = previous). Starts a session there.\",\n\t},\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n\t{ name: \"subagent\", description: \"Spawn a subagent directly: /subagent <mode> <task>\" },\n];\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,wBAAwB,CAAC;AAG9E,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACxC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,uCAAuC;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,gCAAgC;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,CA0L3E","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { type AgentDefinition, TASK_TOOL_NAME } from \"./agent-frontmatter.js\";\nimport { formatAgentsForPrompt } from \"./agent-registry.js\";\nimport { formatSelfDocsForPrompt } from \"./self-docs.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write, search, grep, find, ls] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** Working directory. */\n\tcwd: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/**\n\t * Available agents for delegation, emitted as `<available_agents>` XML.\n\t * Only populated when the Task tool is active so the model knows which\n\t * agents exist without re-reading the agent registry each turn.\n\t */\n\tagents?: AgentDefinition[];\n\t/**\n\t * Point the model at hoocode's own shipped docs so it can answer questions\n\t * about hoocode itself.\n\t *\n\t * Defaults to true for the built-in prompt and false when `customPrompt`\n\t * replaces it. Every other appended section (context files, skills, agents)\n\t * only appears because the caller passed the content in; this one\n\t * materializes on its own, so a caller who has taken over the system prompt\n\t * gets it only by asking. That also keeps it out of light mode, whose whole\n\t * point is a minimal fixed per-turn surface. Needs the read tool either way.\n\t */\n\tincludeSelfDocs?: boolean;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t\tagents: providedAgents,\n\t\tincludeSelfDocs,\n\t} = options;\n\tconst resolvedCwd = cwd;\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst now = new Date();\n\tconst year = now.getFullYear();\n\tconst month = String(now.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(now.getDate()).padStart(2, \"0\");\n\tconst date = `${year}-${month}-${day}`;\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\tconst wantSelfDocs = includeSelfDocs ?? !customPrompt;\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\tconst agents = providedAgents ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst hasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (hasRead && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append agents section (only when Task tool is active)\n\t\tconst hasTask = !selectedTools || selectedTools.includes(TASK_TOOL_NAME);\n\t\tif (hasTask && agents.length > 0) {\n\t\t\tprompt += formatAgentsForPrompt(agents);\n\t\t}\n\n\t\t// Append hoocode's own docs (only if read tool is available)\n\t\tif (wantSelfDocs && hasRead) {\n\t\t\tprompt += formatSelfDocsForPrompt();\n\t\t}\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\"read\", \"bash\", \"edit\", \"write\", \"search\", \"grep\", \"find\", \"ls\"];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasSearch = tools.includes(\"search\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\n\t// File exploration guidelines. Name only the tools that are actually\n\t// registered (the condition used to OR the three but hardcode all three\n\t// names, advertising tools that might not exist) and map each to its job so\n\t// the model picks the right one instead of defaulting to its bash habit.\n\tconst explore: string[] = [];\n\tif (hasSearch) explore.push(\"search (find where code lives by concept or identifier)\");\n\tif (hasGrep) explore.push(\"grep (exact line/regex search)\");\n\tif (hasFind) explore.push(\"find (locate files by name/glob)\");\n\tif (hasLs) explore.push(\"ls (list directory contents)\");\n\tif (explore.length > 0) {\n\t\taddGuideline(\n\t\t\t`For file exploration use the dedicated tools — ${explore.join(\", \")} — instead of bash; they are faster, and respect .gitignore where applicable`,\n\t\t);\n\t} else if (hasBash) {\n\t\taddGuideline(\"Use bash for file exploration (ls, rg/grep, find)\");\n\t}\n\n\t// Single source of truth for the search↔grep decision. Gated on both tools\n\t// being active so we never reference a tool that isn't registered — grep.ts\n\t// and search.ts intentionally no longer cross-reference each other, since a\n\t// tool factory can't know what else is in the bundle.\n\tif (hasSearch && hasGrep) {\n\t\taddGuideline(\n\t\t\t\"Between search and grep: search finds where code lives by concept, behavior, or half-known name (ranked results); grep enumerates exact matching lines, regexes, and counts (output proportional to matches)\",\n\t\t);\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"No preamble or postamble; do not restate the task or summarize what you just did\");\n\taddGuideline('Do not add closers like \"Let me know\" or \"Hope this helps\"');\n\taddGuideline(\n\t\t\"Do not narrate routine tool calls or results — the permission gate already shows them; speak when you have the answer or need a decision\",\n\t);\n\taddGuideline(\n\t\t\"Match the surrounding code's conventions for comments, docstrings, and types — do not add or strip them by default\",\n\t);\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside hoocode, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (hasRead && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append agents section (only when Task tool is active)\n\tconst hasTask = tools.includes(TASK_TOOL_NAME);\n\tif (hasTask && agents.length > 0) {\n\t\tprompt += formatAgentsForPrompt(agents);\n\t}\n\n\t// Append hoocode's own docs (only if read tool is available)\n\tif (wantSelfDocs && hasRead) {\n\t\tprompt += formatSelfDocsForPrompt();\n\t}\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\treturn prompt;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,wBAAwB,CAAC;AAG9E,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACxC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,uCAAuC;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,gCAAgC;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,CA6L3E","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { type AgentDefinition, TASK_TOOL_NAME } from \"./agent-frontmatter.js\";\nimport { formatAgentsForPrompt } from \"./agent-registry.js\";\nimport { formatSelfDocsForPrompt } from \"./self-docs.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write, search, grep, find, ls] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** Working directory. */\n\tcwd: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/**\n\t * Available agents for delegation, emitted as `<available_agents>` XML.\n\t * Only populated when the Task tool is active so the model knows which\n\t * agents exist without re-reading the agent registry each turn.\n\t */\n\tagents?: AgentDefinition[];\n\t/**\n\t * Point the model at hoocode's own shipped docs so it can answer questions\n\t * about hoocode itself.\n\t *\n\t * Defaults to true for the built-in prompt and false when `customPrompt`\n\t * replaces it. Every other appended section (context files, skills, agents)\n\t * only appears because the caller passed the content in; this one\n\t * materializes on its own, so a caller who has taken over the system prompt\n\t * gets it only by asking. That also keeps it out of light mode, whose whole\n\t * point is a minimal fixed per-turn surface. Needs the read tool either way.\n\t */\n\tincludeSelfDocs?: boolean;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t\tagents: providedAgents,\n\t\tincludeSelfDocs,\n\t} = options;\n\tconst resolvedCwd = cwd;\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst now = new Date();\n\tconst year = now.getFullYear();\n\tconst month = String(now.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(now.getDate()).padStart(2, \"0\");\n\tconst date = `${year}-${month}-${day}`;\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\tconst wantSelfDocs = includeSelfDocs ?? !customPrompt;\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\tconst agents = providedAgents ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst hasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (hasRead && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append agents section (only when Task tool is active)\n\t\tconst hasTask = !selectedTools || selectedTools.includes(TASK_TOOL_NAME);\n\t\tif (hasTask && agents.length > 0) {\n\t\t\tprompt += formatAgentsForPrompt(agents);\n\t\t}\n\n\t\t// Append hoocode's own docs (only if read tool is available)\n\t\tif (wantSelfDocs && hasRead) {\n\t\t\tprompt += formatSelfDocsForPrompt();\n\t\t}\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\"read\", \"bash\", \"edit\", \"write\", \"search\", \"grep\", \"find\", \"ls\"];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasSearch = tools.includes(\"search\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\n\t// File exploration guidelines. Name only the tools that are actually\n\t// registered (the condition used to OR the three but hardcode all three\n\t// names, advertising tools that might not exist) and map each to its job so\n\t// the model picks the right one instead of defaulting to its bash habit.\n\tconst explore: string[] = [];\n\tif (hasSearch) explore.push(\"search (find where code lives by concept or identifier)\");\n\tif (hasGrep) explore.push(\"grep (exact line/regex search)\");\n\tif (hasFind) explore.push(\"find (locate files by name/glob)\");\n\tif (hasLs) explore.push(\"ls (list directory contents)\");\n\tif (explore.length > 0) {\n\t\taddGuideline(\n\t\t\t`For file exploration use the dedicated tools — ${explore.join(\", \")} — instead of bash; they are faster, and respect .gitignore where applicable`,\n\t\t);\n\t} else if (hasBash) {\n\t\taddGuideline(\"Use bash for file exploration (ls, rg/grep, find)\");\n\t}\n\n\t// Single source of truth for the search↔grep decision. Gated on both tools\n\t// being active so we never reference a tool that isn't registered — grep.ts\n\t// and search.ts intentionally no longer cross-reference each other, since a\n\t// tool factory can't know what else is in the bundle.\n\tif (hasSearch && hasGrep) {\n\t\taddGuideline(\n\t\t\t\"Between search and grep: search finds where code lives by concept, behavior, or half-known name (ranked results); grep enumerates exact matching lines, regexes, and counts (output proportional to matches)\",\n\t\t);\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\n\t\t\"Put independent tool calls in one message — they execute in parallel; only split them across turns when a call needs an earlier call's result\",\n\t);\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"No preamble or postamble; do not restate the task or summarize what you just did\");\n\taddGuideline('Do not add closers like \"Let me know\" or \"Hope this helps\"');\n\taddGuideline(\n\t\t\"Do not narrate routine tool calls or results — the permission gate already shows them; speak when you have the answer or need a decision\",\n\t);\n\taddGuideline(\n\t\t\"Match the surrounding code's conventions for comments, docstrings, and types — do not add or strip them by default\",\n\t);\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside hoocode, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (hasRead && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append agents section (only when Task tool is active)\n\tconst hasTask = tools.includes(TASK_TOOL_NAME);\n\tif (hasTask && agents.length > 0) {\n\t\tprompt += formatAgentsForPrompt(agents);\n\t}\n\n\t// Append hoocode's own docs (only if read tool is available)\n\tif (wantSelfDocs && hasRead) {\n\t\tprompt += formatSelfDocsForPrompt();\n\t}\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\treturn prompt;\n}\n"]}
|
|
@@ -106,6 +106,7 @@ export function buildSystemPrompt(options) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
// Always include these
|
|
109
|
+
addGuideline("Put independent tool calls in one message — they execute in parallel; only split them across turns when a call needs an earlier call's result");
|
|
109
110
|
addGuideline("Be concise in your responses");
|
|
110
111
|
addGuideline("No preamble or postamble; do not restate the task or summarize what you just did");
|
|
111
112
|
addGuideline('Do not add closers like "Let me know" or "Hope this helps"');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-prompt.js","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { type AgentDefinition, TASK_TOOL_NAME } from \"./agent-frontmatter.js\";\nimport { formatAgentsForPrompt } from \"./agent-registry.js\";\nimport { formatSelfDocsForPrompt } from \"./self-docs.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write, search, grep, find, ls] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** Working directory. */\n\tcwd: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/**\n\t * Available agents for delegation, emitted as `<available_agents>` XML.\n\t * Only populated when the Task tool is active so the model knows which\n\t * agents exist without re-reading the agent registry each turn.\n\t */\n\tagents?: AgentDefinition[];\n\t/**\n\t * Point the model at hoocode's own shipped docs so it can answer questions\n\t * about hoocode itself.\n\t *\n\t * Defaults to true for the built-in prompt and false when `customPrompt`\n\t * replaces it. Every other appended section (context files, skills, agents)\n\t * only appears because the caller passed the content in; this one\n\t * materializes on its own, so a caller who has taken over the system prompt\n\t * gets it only by asking. That also keeps it out of light mode, whose whole\n\t * point is a minimal fixed per-turn surface. Needs the read tool either way.\n\t */\n\tincludeSelfDocs?: boolean;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t\tagents: providedAgents,\n\t\tincludeSelfDocs,\n\t} = options;\n\tconst resolvedCwd = cwd;\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst now = new Date();\n\tconst year = now.getFullYear();\n\tconst month = String(now.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(now.getDate()).padStart(2, \"0\");\n\tconst date = `${year}-${month}-${day}`;\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\tconst wantSelfDocs = includeSelfDocs ?? !customPrompt;\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\tconst agents = providedAgents ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst hasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (hasRead && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append agents section (only when Task tool is active)\n\t\tconst hasTask = !selectedTools || selectedTools.includes(TASK_TOOL_NAME);\n\t\tif (hasTask && agents.length > 0) {\n\t\t\tprompt += formatAgentsForPrompt(agents);\n\t\t}\n\n\t\t// Append hoocode's own docs (only if read tool is available)\n\t\tif (wantSelfDocs && hasRead) {\n\t\t\tprompt += formatSelfDocsForPrompt();\n\t\t}\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\"read\", \"bash\", \"edit\", \"write\", \"search\", \"grep\", \"find\", \"ls\"];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasSearch = tools.includes(\"search\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\n\t// File exploration guidelines. Name only the tools that are actually\n\t// registered (the condition used to OR the three but hardcode all three\n\t// names, advertising tools that might not exist) and map each to its job so\n\t// the model picks the right one instead of defaulting to its bash habit.\n\tconst explore: string[] = [];\n\tif (hasSearch) explore.push(\"search (find where code lives by concept or identifier)\");\n\tif (hasGrep) explore.push(\"grep (exact line/regex search)\");\n\tif (hasFind) explore.push(\"find (locate files by name/glob)\");\n\tif (hasLs) explore.push(\"ls (list directory contents)\");\n\tif (explore.length > 0) {\n\t\taddGuideline(\n\t\t\t`For file exploration use the dedicated tools — ${explore.join(\", \")} — instead of bash; they are faster, and respect .gitignore where applicable`,\n\t\t);\n\t} else if (hasBash) {\n\t\taddGuideline(\"Use bash for file exploration (ls, rg/grep, find)\");\n\t}\n\n\t// Single source of truth for the search↔grep decision. Gated on both tools\n\t// being active so we never reference a tool that isn't registered — grep.ts\n\t// and search.ts intentionally no longer cross-reference each other, since a\n\t// tool factory can't know what else is in the bundle.\n\tif (hasSearch && hasGrep) {\n\t\taddGuideline(\n\t\t\t\"Between search and grep: search finds where code lives by concept, behavior, or half-known name (ranked results); grep enumerates exact matching lines, regexes, and counts (output proportional to matches)\",\n\t\t);\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"No preamble or postamble; do not restate the task or summarize what you just did\");\n\taddGuideline('Do not add closers like \"Let me know\" or \"Hope this helps\"');\n\taddGuideline(\n\t\t\"Do not narrate routine tool calls or results — the permission gate already shows them; speak when you have the answer or need a decision\",\n\t);\n\taddGuideline(\n\t\t\"Match the surrounding code's conventions for comments, docstrings, and types — do not add or strip them by default\",\n\t);\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside hoocode, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (hasRead && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append agents section (only when Task tool is active)\n\tconst hasTask = tools.includes(TASK_TOOL_NAME);\n\tif (hasTask && agents.length > 0) {\n\t\tprompt += formatAgentsForPrompt(agents);\n\t}\n\n\t// Append hoocode's own docs (only if read tool is available)\n\tif (wantSelfDocs && hasRead) {\n\t\tprompt += formatSelfDocsForPrompt();\n\t}\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\treturn prompt;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"system-prompt.js","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { type AgentDefinition, TASK_TOOL_NAME } from \"./agent-frontmatter.js\";\nimport { formatAgentsForPrompt } from \"./agent-registry.js\";\nimport { formatSelfDocsForPrompt } from \"./self-docs.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write, search, grep, find, ls] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** Working directory. */\n\tcwd: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/**\n\t * Available agents for delegation, emitted as `<available_agents>` XML.\n\t * Only populated when the Task tool is active so the model knows which\n\t * agents exist without re-reading the agent registry each turn.\n\t */\n\tagents?: AgentDefinition[];\n\t/**\n\t * Point the model at hoocode's own shipped docs so it can answer questions\n\t * about hoocode itself.\n\t *\n\t * Defaults to true for the built-in prompt and false when `customPrompt`\n\t * replaces it. Every other appended section (context files, skills, agents)\n\t * only appears because the caller passed the content in; this one\n\t * materializes on its own, so a caller who has taken over the system prompt\n\t * gets it only by asking. That also keeps it out of light mode, whose whole\n\t * point is a minimal fixed per-turn surface. Needs the read tool either way.\n\t */\n\tincludeSelfDocs?: boolean;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t\tagents: providedAgents,\n\t\tincludeSelfDocs,\n\t} = options;\n\tconst resolvedCwd = cwd;\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst now = new Date();\n\tconst year = now.getFullYear();\n\tconst month = String(now.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(now.getDate()).padStart(2, \"0\");\n\tconst date = `${year}-${month}-${day}`;\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\tconst wantSelfDocs = includeSelfDocs ?? !customPrompt;\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\tconst agents = providedAgents ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst hasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (hasRead && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append agents section (only when Task tool is active)\n\t\tconst hasTask = !selectedTools || selectedTools.includes(TASK_TOOL_NAME);\n\t\tif (hasTask && agents.length > 0) {\n\t\t\tprompt += formatAgentsForPrompt(agents);\n\t\t}\n\n\t\t// Append hoocode's own docs (only if read tool is available)\n\t\tif (wantSelfDocs && hasRead) {\n\t\t\tprompt += formatSelfDocsForPrompt();\n\t\t}\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\"read\", \"bash\", \"edit\", \"write\", \"search\", \"grep\", \"find\", \"ls\"];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasSearch = tools.includes(\"search\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\n\t// File exploration guidelines. Name only the tools that are actually\n\t// registered (the condition used to OR the three but hardcode all three\n\t// names, advertising tools that might not exist) and map each to its job so\n\t// the model picks the right one instead of defaulting to its bash habit.\n\tconst explore: string[] = [];\n\tif (hasSearch) explore.push(\"search (find where code lives by concept or identifier)\");\n\tif (hasGrep) explore.push(\"grep (exact line/regex search)\");\n\tif (hasFind) explore.push(\"find (locate files by name/glob)\");\n\tif (hasLs) explore.push(\"ls (list directory contents)\");\n\tif (explore.length > 0) {\n\t\taddGuideline(\n\t\t\t`For file exploration use the dedicated tools — ${explore.join(\", \")} — instead of bash; they are faster, and respect .gitignore where applicable`,\n\t\t);\n\t} else if (hasBash) {\n\t\taddGuideline(\"Use bash for file exploration (ls, rg/grep, find)\");\n\t}\n\n\t// Single source of truth for the search↔grep decision. Gated on both tools\n\t// being active so we never reference a tool that isn't registered — grep.ts\n\t// and search.ts intentionally no longer cross-reference each other, since a\n\t// tool factory can't know what else is in the bundle.\n\tif (hasSearch && hasGrep) {\n\t\taddGuideline(\n\t\t\t\"Between search and grep: search finds where code lives by concept, behavior, or half-known name (ranked results); grep enumerates exact matching lines, regexes, and counts (output proportional to matches)\",\n\t\t);\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\n\t\t\"Put independent tool calls in one message — they execute in parallel; only split them across turns when a call needs an earlier call's result\",\n\t);\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"No preamble or postamble; do not restate the task or summarize what you just did\");\n\taddGuideline('Do not add closers like \"Let me know\" or \"Hope this helps\"');\n\taddGuideline(\n\t\t\"Do not narrate routine tool calls or results — the permission gate already shows them; speak when you have the answer or need a decision\",\n\t);\n\taddGuideline(\n\t\t\"Match the surrounding code's conventions for comments, docstrings, and types — do not add or strip them by default\",\n\t);\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside hoocode, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (hasRead && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append agents section (only when Task tool is active)\n\tconst hasTask = tools.includes(TASK_TOOL_NAME);\n\tif (hasTask && agents.length > 0) {\n\t\tprompt += formatAgentsForPrompt(agents);\n\t}\n\n\t// Append hoocode's own docs (only if read tool is available)\n\tif (wantSelfDocs && hasRead) {\n\t\tprompt += formatSelfDocsForPrompt();\n\t}\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\treturn prompt;\n}\n"]}
|
|
@@ -27,9 +27,13 @@ export declare function stripBom(content: string): {
|
|
|
27
27
|
* Apply one or more exact-text replacements to LF-normalized content.
|
|
28
28
|
*
|
|
29
29
|
* All edits are matched against the same original content. Replacements are
|
|
30
|
-
* then applied in reverse order so offsets remain stable.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* then applied in reverse order so offsets remain stable.
|
|
31
|
+
*
|
|
32
|
+
* Every match, however loosely it was found, is resolved back to a span of the
|
|
33
|
+
* *original* content before anything is written. A fuzzy or indentation-tolerant
|
|
34
|
+
* match therefore rewrites only the lines it landed on: the rest of the file
|
|
35
|
+
* keeps its exact bytes, and the diff callers render from `baseContent` is the
|
|
36
|
+
* real change on disk rather than a normalized-vs-normalized view of it.
|
|
33
37
|
*/
|
|
34
38
|
export declare function applyEditsToNormalizedContent(normalizedContent: string, edits: Edit[], path: string): AppliedEditsResult;
|
|
35
39
|
/**
|