@oh-my-pi/pi-coding-agent 14.0.5 → 14.1.1
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 +120 -0
- package/package.json +8 -8
- package/src/async/index.ts +1 -0
- package/src/async/job-manager.ts +43 -10
- package/src/async/support.ts +5 -0
- package/src/cli/list-models.ts +96 -57
- package/src/commit/agentic/tools/analyze-file.ts +1 -2
- package/src/commit/model-selection.ts +16 -13
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-equivalence.ts +675 -0
- package/src/config/model-registry.ts +242 -45
- package/src/config/model-resolver.ts +282 -65
- package/src/config/settings-schema.ts +27 -3
- package/src/config/settings.ts +1 -1
- package/src/cursor.ts +64 -23
- package/src/edit/index.ts +254 -89
- package/src/edit/modes/chunk.ts +336 -57
- package/src/edit/modes/hashline.ts +51 -26
- package/src/edit/modes/patch.ts +16 -10
- package/src/edit/modes/replace.ts +15 -7
- package/src/edit/renderer.ts +248 -94
- package/src/export/html/template.css +82 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +614 -97
- package/src/extensibility/custom-tools/types.ts +0 -3
- package/src/extensibility/extensions/loader.ts +16 -0
- package/src/extensibility/extensions/runner.ts +2 -7
- package/src/extensibility/extensions/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/jobs-protocol.ts +2 -1
- package/src/ipy/executor.ts +447 -52
- package/src/ipy/kernel.ts +39 -13
- package/src/lsp/client.ts +55 -1
- package/src/lsp/index.ts +8 -0
- package/src/lsp/types.ts +6 -0
- package/src/main.ts +6 -2
- package/src/memories/index.ts +7 -6
- package/src/modes/acp/acp-agent.ts +4 -1
- package/src/modes/components/bash-execution.ts +16 -4
- package/src/modes/components/model-selector.ts +221 -64
- package/src/modes/components/status-line/presets.ts +17 -6
- package/src/modes/components/status-line/segments.ts +15 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +7 -1
- package/src/modes/components/tool-execution.ts +145 -75
- package/src/modes/controllers/command-controller.ts +42 -1
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +28 -5
- package/src/modes/controllers/input-controller.ts +9 -3
- package/src/modes/controllers/selector-controller.ts +17 -6
- package/src/modes/interactive-mode.ts +19 -3
- package/src/modes/print-mode.ts +13 -4
- package/src/modes/prompt-action-autocomplete.ts +3 -5
- package/src/modes/rpc/rpc-mode.ts +8 -2
- package/src/modes/shared.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +16 -1
- package/src/prompts/tools/cancel-job.md +1 -1
- package/src/prompts/tools/chunk-edit.md +191 -163
- package/src/prompts/tools/hashline.md +11 -11
- package/src/prompts/tools/patch.md +10 -5
- package/src/prompts/tools/{await.md → poll.md} +1 -1
- package/src/prompts/tools/read-chunk.md +12 -3
- package/src/prompts/tools/read.md +9 -0
- package/src/prompts/tools/task.md +2 -2
- package/src/prompts/tools/vim.md +98 -0
- package/src/prompts/tools/write.md +1 -0
- package/src/sdk.ts +758 -725
- package/src/session/agent-session.ts +187 -40
- package/src/session/session-manager.ts +50 -4
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/task/executor.ts +9 -5
- package/src/task/index.ts +3 -5
- package/src/task/types.ts +2 -2
- package/src/tools/bash.ts +240 -57
- package/src/tools/cancel-job.ts +2 -1
- package/src/tools/find.ts +5 -2
- package/src/tools/grep.ts +77 -8
- package/src/tools/index.ts +48 -19
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/{await-tool.ts → poll-tool.ts} +38 -31
- package/src/tools/python.ts +293 -278
- package/src/tools/read.ts +218 -1
- package/src/tools/sqlite-reader.ts +623 -0
- package/src/tools/submit-result.ts +5 -2
- package/src/tools/todo-write.ts +8 -2
- package/src/tools/vim.ts +966 -0
- package/src/tools/write.ts +187 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/edit-mode.ts +2 -1
- package/src/utils/git.ts +24 -1
- package/src/utils/session-color.ts +55 -0
- package/src/utils/title-generator.ts +16 -7
- package/src/vim/buffer.ts +309 -0
- package/src/vim/commands.ts +382 -0
- package/src/vim/engine.ts +2426 -0
- package/src/vim/parser.ts +151 -0
- package/src/vim/render.ts +252 -0
- package/src/vim/types.ts +197 -0
package/src/tools/write.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type {
|
|
@@ -9,7 +10,7 @@ import type {
|
|
|
9
10
|
} from "@oh-my-pi/pi-agent-core";
|
|
10
11
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
11
12
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
12
|
-
import { isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
13
|
+
import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
13
14
|
import { type Static, Type } from "@sinclair/typebox";
|
|
14
15
|
import { unzipSync, zipSync } from "fflate";
|
|
15
16
|
import { stripHashlinePrefixes } from "../edit";
|
|
@@ -34,7 +35,18 @@ import {
|
|
|
34
35
|
replaceTabs,
|
|
35
36
|
shortenPath,
|
|
36
37
|
} from "./render-utils";
|
|
38
|
+
import {
|
|
39
|
+
deleteRowByKey,
|
|
40
|
+
deleteRowByRowId,
|
|
41
|
+
insertRow,
|
|
42
|
+
isSqliteFile,
|
|
43
|
+
parseSqlitePathCandidates,
|
|
44
|
+
resolveTableRowLookup,
|
|
45
|
+
updateRowByKey,
|
|
46
|
+
updateRowByRowId,
|
|
47
|
+
} from "./sqlite-reader";
|
|
37
48
|
import { ToolError } from "./tool-errors";
|
|
49
|
+
import { toolResult } from "./tool-result";
|
|
38
50
|
|
|
39
51
|
const writeSchema = Type.Object({
|
|
40
52
|
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
|
|
@@ -94,6 +106,14 @@ interface ResolvedArchiveWritePath {
|
|
|
94
106
|
exists: boolean;
|
|
95
107
|
}
|
|
96
108
|
|
|
109
|
+
interface ResolvedSqliteWritePath {
|
|
110
|
+
absolutePath: string;
|
|
111
|
+
sqlitePath: string;
|
|
112
|
+
table: string;
|
|
113
|
+
key?: string;
|
|
114
|
+
exists: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
97
117
|
function isArchivePathNotFound(error: unknown): boolean {
|
|
98
118
|
if (isEnoent(error)) return true;
|
|
99
119
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOTDIR";
|
|
@@ -125,6 +145,29 @@ function normalizeArchiveWriteSubPath(rawPath: string): string {
|
|
|
125
145
|
return normalizedParts.join("/");
|
|
126
146
|
}
|
|
127
147
|
|
|
148
|
+
function parseSqliteWriteTarget(subPath: string, queryString: string): { table: string; key?: string } {
|
|
149
|
+
if (queryString.trim().length > 0) {
|
|
150
|
+
throw new ToolError("SQLite write paths do not support query parameters");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const normalized = subPath.replace(/^:+/, "").trim();
|
|
154
|
+
if (!normalized) {
|
|
155
|
+
throw new ToolError("SQLite write path must target a table");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const separatorIndex = normalized.indexOf(":");
|
|
159
|
+
const table = separatorIndex === -1 ? normalized : normalized.slice(0, separatorIndex);
|
|
160
|
+
const key = separatorIndex === -1 ? undefined : normalized.slice(separatorIndex + 1);
|
|
161
|
+
if (!table) {
|
|
162
|
+
throw new ToolError("SQLite write path must target a table");
|
|
163
|
+
}
|
|
164
|
+
if (key !== undefined && key.length === 0) {
|
|
165
|
+
throw new ToolError("SQLite row writes require a non-empty row key");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { table, key };
|
|
169
|
+
}
|
|
170
|
+
|
|
128
171
|
/**
|
|
129
172
|
* Write tool implementation.
|
|
130
173
|
*
|
|
@@ -262,6 +305,132 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
262
305
|
};
|
|
263
306
|
}
|
|
264
307
|
|
|
308
|
+
async #resolveSqliteWritePath(writePath: string): Promise<ResolvedSqliteWritePath | null> {
|
|
309
|
+
const candidates = parseSqlitePathCandidates(writePath).filter(candidate => candidate.sqlitePath !== writePath);
|
|
310
|
+
if (candidates.length === 0) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const fallbackCandidate = candidates[candidates.length - 1]!;
|
|
315
|
+
const fallbackTarget = parseSqliteWriteTarget(fallbackCandidate.subPath, fallbackCandidate.queryString);
|
|
316
|
+
const fallback: ResolvedSqliteWritePath = {
|
|
317
|
+
absolutePath: resolvePlanPath(this.session, fallbackCandidate.sqlitePath),
|
|
318
|
+
sqlitePath: fallbackCandidate.sqlitePath,
|
|
319
|
+
table: fallbackTarget.table,
|
|
320
|
+
key: fallbackTarget.key,
|
|
321
|
+
exists: false,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
let sawExistingNonSqlite = false;
|
|
325
|
+
for (const candidate of candidates) {
|
|
326
|
+
const target = parseSqliteWriteTarget(candidate.subPath, candidate.queryString);
|
|
327
|
+
const absolutePath = resolvePlanPath(this.session, candidate.sqlitePath);
|
|
328
|
+
try {
|
|
329
|
+
const stat = await Bun.file(absolutePath).stat();
|
|
330
|
+
if (stat.isDirectory()) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (!(await isSqliteFile(absolutePath))) {
|
|
334
|
+
sawExistingNonSqlite = true;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
absolutePath,
|
|
340
|
+
sqlitePath: candidate.sqlitePath,
|
|
341
|
+
table: target.table,
|
|
342
|
+
key: target.key,
|
|
343
|
+
exists: true,
|
|
344
|
+
};
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (!isArchivePathNotFound(error)) {
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (sawExistingNonSqlite) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return fallback;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async #writeSqliteRow(
|
|
360
|
+
displayPath: string,
|
|
361
|
+
content: string,
|
|
362
|
+
resolvedSqlitePath: ResolvedSqliteWritePath,
|
|
363
|
+
): Promise<AgentToolResult<WriteToolDetails>> {
|
|
364
|
+
let db: Database | null = null;
|
|
365
|
+
try {
|
|
366
|
+
if (!resolvedSqlitePath.exists) {
|
|
367
|
+
throw new ToolError(`SQLite database '${displayPath}' not found`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
db = new Database(resolvedSqlitePath.absolutePath, { create: false, strict: true });
|
|
371
|
+
db.run("PRAGMA busy_timeout = 3000");
|
|
372
|
+
|
|
373
|
+
const trimmedContent = content.trim();
|
|
374
|
+
let resultText: string;
|
|
375
|
+
if (trimmedContent.length === 0) {
|
|
376
|
+
if (!resolvedSqlitePath.key) {
|
|
377
|
+
throw new ToolError("SQLite deletes require a row key in the path");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const lookup = resolveTableRowLookup(db, resolvedSqlitePath.table);
|
|
381
|
+
const deleted =
|
|
382
|
+
lookup.kind === "pk"
|
|
383
|
+
? deleteRowByKey(db, resolvedSqlitePath.table, lookup, resolvedSqlitePath.key)
|
|
384
|
+
: deleteRowByRowId(db, resolvedSqlitePath.table, resolvedSqlitePath.key);
|
|
385
|
+
resultText =
|
|
386
|
+
deleted > 0
|
|
387
|
+
? `Deleted row '${resolvedSqlitePath.key}' from ${resolvedSqlitePath.table}`
|
|
388
|
+
: `No row deleted from ${resolvedSqlitePath.table} for key '${resolvedSqlitePath.key}'`;
|
|
389
|
+
} else {
|
|
390
|
+
let parsedContent: unknown;
|
|
391
|
+
try {
|
|
392
|
+
parsedContent = Bun.JSON5.parse(content);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
throw new ToolError(
|
|
395
|
+
`SQLite write content must be valid JSON5: ${error instanceof Error ? error.message : String(error)}`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!isRecord(parsedContent)) {
|
|
400
|
+
throw new ToolError("SQLite write content must be a JSON object");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (resolvedSqlitePath.key) {
|
|
404
|
+
const lookup = resolveTableRowLookup(db, resolvedSqlitePath.table);
|
|
405
|
+
const updated =
|
|
406
|
+
lookup.kind === "pk"
|
|
407
|
+
? updateRowByKey(db, resolvedSqlitePath.table, lookup, resolvedSqlitePath.key, parsedContent)
|
|
408
|
+
: updateRowByRowId(db, resolvedSqlitePath.table, resolvedSqlitePath.key, parsedContent);
|
|
409
|
+
resultText =
|
|
410
|
+
updated > 0
|
|
411
|
+
? `Updated row '${resolvedSqlitePath.key}' in ${resolvedSqlitePath.table}`
|
|
412
|
+
: `No row updated in ${resolvedSqlitePath.table} for key '${resolvedSqlitePath.key}'`;
|
|
413
|
+
} else {
|
|
414
|
+
insertRow(db, resolvedSqlitePath.table, parsedContent);
|
|
415
|
+
resultText = `Inserted row into ${resolvedSqlitePath.table}`;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
invalidateFsScanAfterWrite(resolvedSqlitePath.absolutePath);
|
|
420
|
+
return toolResult<WriteToolDetails>({}).text(resultText).sourcePath(resolvedSqlitePath.absolutePath).done();
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (isEnoent(error)) {
|
|
423
|
+
throw new ToolError(`SQLite database '${displayPath}' not found`);
|
|
424
|
+
}
|
|
425
|
+
if (error instanceof ToolError) {
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
429
|
+
} finally {
|
|
430
|
+
db?.close();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
265
434
|
async execute(
|
|
266
435
|
_toolCallId: string,
|
|
267
436
|
{ path, content }: WriteParams,
|
|
@@ -291,6 +460,23 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
291
460
|
return archiveResult;
|
|
292
461
|
}
|
|
293
462
|
|
|
463
|
+
const resolvedSqlitePath = await this.#resolveSqliteWritePath(path);
|
|
464
|
+
if (resolvedSqlitePath) {
|
|
465
|
+
enforcePlanModeWrite(this.session, resolvedSqlitePath.sqlitePath, { op: "update" });
|
|
466
|
+
|
|
467
|
+
const sqliteResult = await this.#writeSqliteRow(path, cleanContent, resolvedSqlitePath);
|
|
468
|
+
if (stripped) {
|
|
469
|
+
const firstText = sqliteResult.content.find(
|
|
470
|
+
(block): block is { type: "text"; text: string } =>
|
|
471
|
+
block.type === "text" && typeof block.text === "string",
|
|
472
|
+
);
|
|
473
|
+
if (firstText) {
|
|
474
|
+
firstText.text += `\nNote: auto-stripped hashline display prefixes from content before writing.`;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return sqliteResult;
|
|
478
|
+
}
|
|
479
|
+
|
|
294
480
|
enforcePlanModeWrite(this.session, path, { op: "create" });
|
|
295
481
|
const absolutePath = resolvePlanPath(this.session, path);
|
|
296
482
|
const batchRequest = getLspBatchRequest(context?.toolCall);
|
|
@@ -52,6 +52,7 @@ function getSmolModelCandidates(
|
|
|
52
52
|
const configuredSmol = resolveModelRoleValue(settings.getModelRole("smol"), availableModels, {
|
|
53
53
|
settings,
|
|
54
54
|
matchPreferences,
|
|
55
|
+
modelRegistry: registry,
|
|
55
56
|
});
|
|
56
57
|
addCandidate(configuredSmol.model, configuredSmol.thinkingLevel);
|
|
57
58
|
|
package/src/utils/edit-mode.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { $env, $flag } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
|
-
export type EditMode = "replace" | "patch" | "hashline" | "chunk";
|
|
3
|
+
export type EditMode = "replace" | "patch" | "hashline" | "chunk" | "vim";
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_EDIT_MODE: EditMode = "hashline";
|
|
6
6
|
|
|
@@ -9,6 +9,7 @@ const EDIT_MODE_IDS = {
|
|
|
9
9
|
hashline: "hashline",
|
|
10
10
|
patch: "patch",
|
|
11
11
|
replace: "replace",
|
|
12
|
+
vim: "vim",
|
|
12
13
|
} as const satisfies Record<string, EditMode>;
|
|
13
14
|
|
|
14
15
|
export const EDIT_MODES = Object.keys(EDIT_MODE_IDS) as EditMode[];
|
package/src/utils/git.ts
CHANGED
|
@@ -146,6 +146,10 @@ const NO_OPTIONAL_LOCKS = "--no-optional-locks";
|
|
|
146
146
|
const HEAD_REF_PREFIX = "ref:";
|
|
147
147
|
const LOCAL_BRANCH_PREFIX = "refs/heads/";
|
|
148
148
|
const DEFAULT_BRANCH_REFS = ["refs/remotes/origin/HEAD", "refs/remotes/upstream/HEAD"] as const;
|
|
149
|
+
const SHORT_LIVED_GIT_CONFIG: readonly (readonly [key: string, value: string])[] = [
|
|
150
|
+
["core.fsmonitor", "false"],
|
|
151
|
+
["core.untrackedCache", "false"],
|
|
152
|
+
];
|
|
149
153
|
|
|
150
154
|
interface CommandOptions {
|
|
151
155
|
readonly env?: Record<string, string | undefined>;
|
|
@@ -183,7 +187,7 @@ async function runCommand(
|
|
|
183
187
|
args: readonly string[],
|
|
184
188
|
options: CommandOptions = {},
|
|
185
189
|
): Promise<GitCommandResult> {
|
|
186
|
-
const commandArgs = options.readOnly ? withNoOptionalLocks(args) : [...args];
|
|
190
|
+
const commandArgs = withShortLivedGitConfig(options.readOnly ? withNoOptionalLocks(args) : [...args]);
|
|
187
191
|
const child = Bun.spawn(["git", ...commandArgs], {
|
|
188
192
|
cwd,
|
|
189
193
|
env: options.env ? { ...process.env, ...options.env } : undefined,
|
|
@@ -212,6 +216,25 @@ function withNoOptionalLocks(args: readonly string[]): string[] {
|
|
|
212
216
|
return [NO_OPTIONAL_LOCKS, ...args];
|
|
213
217
|
}
|
|
214
218
|
|
|
219
|
+
function withShortLivedGitConfig(args: readonly string[]): string[] {
|
|
220
|
+
const prefix: string[] = [];
|
|
221
|
+
for (const [key, value] of SHORT_LIVED_GIT_CONFIG) {
|
|
222
|
+
if (hasGitConfig(args, key, value)) continue;
|
|
223
|
+
prefix.push("-c", `${key}=${value}`);
|
|
224
|
+
}
|
|
225
|
+
return [...prefix, ...args];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function hasGitConfig(args: readonly string[], key: string, value: string): boolean {
|
|
229
|
+
const expected = `${key}=${value}`;
|
|
230
|
+
for (let index = 0; index < args.length - 1; index += 1) {
|
|
231
|
+
if (args[index] === "-c" && args[index + 1] === expected) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
215
238
|
async function runChecked(
|
|
216
239
|
cwd: string,
|
|
217
240
|
args: readonly string[],
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a stable hue (0-359) from a string using djb2 hash.
|
|
3
|
+
*/
|
|
4
|
+
function nameToHue(name: string): number {
|
|
5
|
+
let hash = 5381;
|
|
6
|
+
for (let i = 0; i < name.length; i++) {
|
|
7
|
+
hash = ((hash << 5) + hash) ^ name.charCodeAt(i);
|
|
8
|
+
hash = hash >>> 0; // keep 32-bit unsigned
|
|
9
|
+
}
|
|
10
|
+
return hash % 360;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert HSL (h: 0-360, s: 0-1, l: 0-1) to a CSS hex string.
|
|
15
|
+
*/
|
|
16
|
+
function hslToHex(h: number, s: number, l: number): string {
|
|
17
|
+
const a = s * Math.min(l, 1 - l);
|
|
18
|
+
const f = (n: number) => {
|
|
19
|
+
const k = (n + h / 30) % 12;
|
|
20
|
+
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
21
|
+
return Math.round(255 * color)
|
|
22
|
+
.toString(16)
|
|
23
|
+
.padStart(2, "0");
|
|
24
|
+
};
|
|
25
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Derive a stable CSS hex accent color from a session name.
|
|
30
|
+
* High saturation, vivid — suitable for both status bar text and border coloring.
|
|
31
|
+
*/
|
|
32
|
+
export function getSessionAccentHex(name: string): string {
|
|
33
|
+
return hslToHex(nameToHue(name), 0.9, 0.72);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Auto-generated titles should not drive the session accent.
|
|
38
|
+
* Legacy sessions with unknown title source keep the old behavior.
|
|
39
|
+
*/
|
|
40
|
+
export function getSessionAccentHexForTitle(
|
|
41
|
+
name: string | undefined,
|
|
42
|
+
titleSource: "auto" | "user" | undefined,
|
|
43
|
+
): string | undefined {
|
|
44
|
+
if (!name || titleSource === "auto") return undefined;
|
|
45
|
+
return getSessionAccentHex(name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert a hex accent color to an ANSI-16m foreground escape sequence.
|
|
50
|
+
* Returns `undefined` if `hex` is nullish or Bun.color conversion fails.
|
|
51
|
+
*/
|
|
52
|
+
export function getSessionAccentAnsi(hex: string | undefined): string | undefined {
|
|
53
|
+
if (!hex) return undefined;
|
|
54
|
+
return Bun.color(hex, "ansi-16m") ?? undefined;
|
|
55
|
+
}
|
|
@@ -27,7 +27,7 @@ function getTitleModel(
|
|
|
27
27
|
const availableModels = registry.getAvailable();
|
|
28
28
|
if (availableModels.length === 0) return undefined;
|
|
29
29
|
|
|
30
|
-
const titleModel = resolveRoleSelection(["commit", "smol"], settings, availableModels);
|
|
30
|
+
const titleModel = resolveRoleSelection(["commit", "smol"], settings, availableModels, registry);
|
|
31
31
|
if (titleModel) {
|
|
32
32
|
return { model: titleModel.model, thinkingLevel: titleModel.thinkingLevel };
|
|
33
33
|
}
|
|
@@ -153,20 +153,29 @@ function getFallbackTerminalTitle(cwd: string | undefined): string | undefined {
|
|
|
153
153
|
return sanitizeTerminalTitlePart(baseName);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
export function formatSessionTerminalTitle(
|
|
157
|
-
|
|
156
|
+
export function formatSessionTerminalTitle(
|
|
157
|
+
sessionName: string | undefined,
|
|
158
|
+
cwd?: string,
|
|
159
|
+
titleSource?: "auto" | "user" | undefined,
|
|
160
|
+
): string {
|
|
161
|
+
const label =
|
|
162
|
+
sanitizeTerminalTitlePart(titleSource === "auto" ? undefined : sessionName) ?? getFallbackTerminalTitle(cwd);
|
|
158
163
|
return label ? `${DEFAULT_TERMINAL_TITLE}: ${label}` : DEFAULT_TERMINAL_TITLE;
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
/**
|
|
162
|
-
* Set the terminal title using OSC
|
|
167
|
+
* Set the terminal title using OSC 0 (sets both tab and window title). Unsupported terminals ignore it.
|
|
163
168
|
*/
|
|
164
169
|
export function setTerminalTitle(title: string): void {
|
|
165
|
-
process.stdout.write(`\x1b]
|
|
170
|
+
process.stdout.write(`\x1b]0;${sanitizeTerminalTitlePart(title) ?? DEFAULT_TERMINAL_TITLE}\x07`);
|
|
166
171
|
}
|
|
167
172
|
|
|
168
|
-
export function setSessionTerminalTitle(
|
|
169
|
-
|
|
173
|
+
export function setSessionTerminalTitle(
|
|
174
|
+
sessionName: string | undefined,
|
|
175
|
+
cwd?: string,
|
|
176
|
+
titleSource?: "auto" | "user" | undefined,
|
|
177
|
+
): void {
|
|
178
|
+
setTerminalTitle(formatSessionTerminalTitle(sessionName, cwd, titleSource));
|
|
170
179
|
}
|
|
171
180
|
|
|
172
181
|
/**
|