@oh-my-pi/pi-coding-agent 15.11.3 → 15.11.4
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 +54 -0
- package/dist/cli.js +353 -294
- package/dist/types/config/api-key-resolver.d.ts +9 -3
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-discovery.d.ts +6 -4
- package/dist/types/config/model-registry.d.ts +7 -4
- package/dist/types/config/settings-schema.d.ts +458 -155
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/mnemopi/config.d.ts +3 -1
- package/dist/types/modes/components/settings-defs.d.ts +9 -2
- package/dist/types/modes/components/settings-selector.d.ts +9 -4
- package/dist/types/modes/components/tool-execution.d.ts +12 -1
- package/dist/types/modes/components/transcript-container.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +9 -1
- package/dist/types/modes/theme/theme.d.ts +23 -3
- package/dist/types/session/agent-session.d.ts +14 -7
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/snapcompact-inline.d.ts +28 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
- package/dist/types/system-prompt.d.ts +3 -1
- package/dist/types/task/render.d.ts +16 -6
- package/dist/types/tools/gh.d.ts +3 -0
- package/dist/types/tools/render-utils.d.ts +8 -16
- package/dist/types/utils/session-color.d.ts +15 -3
- package/dist/types/web/kagi.d.ts +1 -2
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/dist/types/web/search/providers/gemini.d.ts +9 -6
- package/package.json +11 -11
- package/src/auto-thinking/classifier.ts +1 -5
- package/src/commit/model-selection.ts +3 -6
- package/src/config/api-key-resolver.ts +10 -3
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-discovery.ts +60 -46
- package/src/config/model-registry.ts +21 -8
- package/src/config/model-resolver.ts +57 -3
- package/src/config/settings-schema.ts +601 -153
- package/src/eval/completion-bridge.ts +1 -5
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +13 -6
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/issue-pr-protocol.ts +10 -4
- package/src/memories/index.ts +2 -10
- package/src/mnemopi/backend.ts +30 -8
- package/src/mnemopi/config.ts +6 -1
- package/src/mnemopi/state.ts +6 -0
- package/src/modes/components/extensions/inspector-panel.ts +6 -2
- package/src/modes/components/plan-review-overlay.ts +15 -17
- package/src/modes/components/plugin-settings.ts +22 -5
- package/src/modes/components/settings-defs.ts +19 -4
- package/src/modes/components/settings-selector.ts +493 -93
- package/src/modes/components/status-line/component.ts +3 -1
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tool-execution.ts +69 -12
- package/src/modes/components/transcript-container.ts +26 -0
- package/src/modes/components/tree-selector.ts +16 -6
- package/src/modes/controllers/command-controller.ts +37 -7
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +68 -6
- package/src/modes/controllers/selector-controller.ts +81 -61
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/rpc/rpc-mode.ts +2 -1
- package/src/modes/shared.ts +2 -0
- package/src/modes/theme/theme.ts +100 -7
- package/src/modes/utils/context-usage.ts +3 -1
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +9 -5
- package/src/prompts/system/personalities/default.md +26 -0
- package/src/prompts/system/personalities/friendly.md +17 -0
- package/src/prompts/system/personalities/pragmatic.md +15 -0
- package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-system-stub.md +1 -0
- package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
- package/src/prompts/system/system-prompt.md +5 -22
- package/src/prompts/tools/task.md +3 -3
- package/src/sdk.ts +22 -1
- package/src/session/agent-session.ts +91 -24
- package/src/session/auth-storage.ts +1 -0
- package/src/session/session-dump-format.ts +8 -1
- package/src/session/session-manager.ts +5 -5
- package/src/session/snapcompact-inline.ts +187 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +24 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/render.ts +29 -19
- package/src/tool-discovery/tool-index.ts +2 -0
- package/src/tools/bash.ts +10 -3
- package/src/tools/eval-render.ts +13 -8
- package/src/tools/gh.ts +39 -1
- package/src/tools/image-gen.ts +114 -78
- package/src/tools/inspect-image.ts +1 -5
- package/src/tools/job.ts +25 -5
- package/src/tools/read.ts +1 -57
- package/src/tools/render-utils.ts +29 -31
- package/src/tools/ssh.ts +3 -3
- package/src/tools/tts.ts +40 -20
- package/src/utils/clipboard.ts +56 -4
- package/src/utils/commit-message-generator.ts +1 -5
- package/src/utils/session-color.ts +83 -9
- package/src/utils/title-generator.ts +1 -1
- package/src/web/kagi.ts +26 -27
- package/src/web/search/providers/codex.ts +42 -40
- package/src/web/search/providers/gemini.ts +42 -22
- package/src/web/search/providers/perplexity.ts +22 -10
package/src/tools/job.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { prompt } from "@oh-my-pi/pi-utils";
|
|
|
5
5
|
import * as z from "zod/v4";
|
|
6
6
|
import type { AsyncJob, AsyncJobManager } from "../async";
|
|
7
7
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
|
+
import { shimmerEnabled, shimmerText } from "../modes/theme/shimmer";
|
|
8
9
|
import type { Theme } from "../modes/theme/theme";
|
|
9
10
|
import jobDescription from "../prompts/tools/job.md" with { type: "text" };
|
|
10
11
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
@@ -493,8 +494,15 @@ export const jobToolRenderer = {
|
|
|
493
494
|
render(width: number): readonly string[] {
|
|
494
495
|
const expanded = options.expanded;
|
|
495
496
|
const spinnerFrame = options.spinnerFrame ?? 0;
|
|
496
|
-
|
|
497
|
-
|
|
497
|
+
// Running-job labels shimmer while the poll block is live; the band
|
|
498
|
+
// phase is Date.now()-sampled at render time, so serving cached bytes
|
|
499
|
+
// would pin it to the ~12.5fps spinner-glyph cadence instead of the
|
|
500
|
+
// 30fps redraw. Bypass the cache while any row animates, and key on
|
|
501
|
+
// the animation state so a sealed block never hits stale shimmered
|
|
502
|
+
// bytes (spinnerFrame falls back to 0 on both sides of the seal).
|
|
503
|
+
const shimmerActive = counts.running > 0 && options.spinnerFrame !== undefined && shimmerEnabled();
|
|
504
|
+
const key = new Hasher().bool(expanded).u32(width).u32(spinnerFrame).bool(shimmerActive).digest();
|
|
505
|
+
if (!shimmerActive && cached?.key === key) return cached.lines;
|
|
498
506
|
|
|
499
507
|
const itemLines = renderTreeList<JobSnapshot>(
|
|
500
508
|
{
|
|
@@ -513,7 +521,9 @@ export const jobToolRenderer = {
|
|
|
513
521
|
job.status === "running" ? options.spinnerFrame : undefined,
|
|
514
522
|
);
|
|
515
523
|
const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
|
|
516
|
-
|
|
524
|
+
// Task jobs label themselves with their agent id, which is also
|
|
525
|
+
// the job id — drop the id column instead of stuttering it twice.
|
|
526
|
+
const idPart = job.label.trim() === job.id ? "" : ` ${uiTheme.fg("muted", job.id)}`;
|
|
517
527
|
const rawLabelLines = (job.label || "(no label)").split(/\r?\n/);
|
|
518
528
|
const maxLabelLines = expanded ? LABEL_LINES_EXPANDED : LABEL_LINES_COLLAPSED;
|
|
519
529
|
const visibleLabelLines = rawLabelLines
|
|
@@ -524,8 +534,18 @@ export const jobToolRenderer = {
|
|
|
524
534
|
visibleLabelLines[visibleLabelLines.length - 1] = `${last} …`;
|
|
525
535
|
}
|
|
526
536
|
const durationText = uiTheme.fg("dim", formatDuration(job.durationMs));
|
|
527
|
-
|
|
528
|
-
|
|
537
|
+
// Running rows in a live block shimmer their label; once the block
|
|
538
|
+
// stops animating (sealed, or a settled snapshot — spinnerFrame
|
|
539
|
+
// cleared) they render static so scrollback never keeps a mid-sweep
|
|
540
|
+
// shimmer band.
|
|
541
|
+
const live = job.status === "running" && options.spinnerFrame !== undefined;
|
|
542
|
+
const headRaw = visibleLabelLines[0] ?? "";
|
|
543
|
+
const headLabel = live
|
|
544
|
+
? shimmerEnabled()
|
|
545
|
+
? shimmerText(headRaw, uiTheme)
|
|
546
|
+
: uiTheme.fg("accent", headRaw)
|
|
547
|
+
: uiTheme.fg("toolOutput", headRaw);
|
|
548
|
+
lines.push(`${icon}${idPart} ${typeBadge} ${headLabel} ${durationText}`);
|
|
529
549
|
for (let i = 1; i < visibleLabelLines.length; i++) {
|
|
530
550
|
lines.push(` ${uiTheme.fg("toolOutput", visibleLabelLines[i]!)}`);
|
|
531
551
|
}
|
package/src/tools/read.ts
CHANGED
|
@@ -736,17 +736,6 @@ interface ResolvedSqliteReadPath {
|
|
|
736
736
|
/** Per-execute memo of suffix-glob lookups; `null` records a confirmed miss. */
|
|
737
737
|
type SuffixMatchCache = Map<string, { absolutePath: string; displayPath: string } | null>;
|
|
738
738
|
|
|
739
|
-
/**
|
|
740
|
-
* Repeated whole-file reads of the same path pin stale copies in context.
|
|
741
|
-
* From this per-session read count onward, file reads carry a trailing nudge
|
|
742
|
-
* to prefer narrower re-reads.
|
|
743
|
-
*/
|
|
744
|
-
const REPEAT_READ_NOTICE_THRESHOLD = 3;
|
|
745
|
-
|
|
746
|
-
function formatRepeatReadNotice(count: number): string {
|
|
747
|
-
return `[note: read #${count} of this file this session — after edits, prefer the context echoed in the edit result or a narrow range re-read]`;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
739
|
/**
|
|
751
740
|
* Read tool implementation.
|
|
752
741
|
*
|
|
@@ -765,8 +754,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
765
754
|
readonly #autoResizeImages: boolean;
|
|
766
755
|
readonly #defaultLimit: number;
|
|
767
756
|
readonly #inspectImageEnabled: boolean;
|
|
768
|
-
/** Successful file reads per resolved base path (selector stripped) this session. */
|
|
769
|
-
readonly #readCounts = new Map<string, number>();
|
|
770
757
|
|
|
771
758
|
constructor(private readonly session: ToolSession) {
|
|
772
759
|
const displayMode = resolveFileDisplayMode(session);
|
|
@@ -785,19 +772,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
785
772
|
});
|
|
786
773
|
}
|
|
787
774
|
|
|
788
|
-
/**
|
|
789
|
-
* Count a file read of `absolutePath` and return the repeat-read nudge once
|
|
790
|
-
* the per-session count reaches {@link REPEAT_READ_NOTICE_THRESHOLD}.
|
|
791
|
-
* Non-file sources (URLs, internal resources, directories, archives,
|
|
792
|
-
* SQLite, images) are never counted.
|
|
793
|
-
*/
|
|
794
|
-
#repeatReadNotice(absolutePath: string): string | undefined {
|
|
795
|
-
const count = (this.#readCounts.get(absolutePath) ?? 0) + 1;
|
|
796
|
-
this.#readCounts.set(absolutePath, count);
|
|
797
|
-
if (count < REPEAT_READ_NOTICE_THRESHOLD) return undefined;
|
|
798
|
-
return formatRepeatReadNotice(count);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
775
|
async #tryReadDelimitedPaths(
|
|
802
776
|
readPath: string,
|
|
803
777
|
signal?: AbortSignal,
|
|
@@ -974,8 +948,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
974
948
|
ignoreResultLimits?: boolean;
|
|
975
949
|
raw?: boolean;
|
|
976
950
|
immutable?: boolean;
|
|
977
|
-
/** Trailing repeat-read nudge; appended at the very end of the text. */
|
|
978
|
-
repeatNotice?: string;
|
|
979
951
|
},
|
|
980
952
|
): AgentToolResult<ReadToolDetails> {
|
|
981
953
|
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
@@ -1120,9 +1092,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1120
1092
|
: formatLineEntries(buildLineEntries(endLine), startLineDisplay);
|
|
1121
1093
|
}
|
|
1122
1094
|
|
|
1123
|
-
if (options.repeatNotice) {
|
|
1124
|
-
outputText += `\n${options.repeatNotice}`;
|
|
1125
|
-
}
|
|
1126
1095
|
resultBuilder.text(outputText);
|
|
1127
1096
|
if (truncationInfo) {
|
|
1128
1097
|
resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
@@ -1148,8 +1117,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1148
1117
|
entityLabel: string;
|
|
1149
1118
|
raw?: boolean;
|
|
1150
1119
|
immutable?: boolean;
|
|
1151
|
-
/** Trailing repeat-read nudge; appended at the very end of the text. */
|
|
1152
|
-
repeatNotice?: string;
|
|
1153
1120
|
},
|
|
1154
1121
|
): AgentToolResult<ReadToolDetails> {
|
|
1155
1122
|
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
@@ -1210,11 +1177,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1210
1177
|
const bound = range.endLine !== undefined ? `${range.startLine}-${range.endLine}` : `${range.startLine}`;
|
|
1211
1178
|
notices.push(`[Range ${bound} is beyond end of ${options.entityLabel} (${totalLines} lines total); skipped]`);
|
|
1212
1179
|
}
|
|
1213
|
-
|
|
1180
|
+
const finalText =
|
|
1214
1181
|
notices.length > 0 ? (outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n")) : outputText;
|
|
1215
|
-
if (options.repeatNotice) {
|
|
1216
|
-
finalText = finalText ? `${finalText}\n${options.repeatNotice}` : options.repeatNotice;
|
|
1217
|
-
}
|
|
1218
1182
|
resultBuilder.text(finalText);
|
|
1219
1183
|
return resultBuilder.done();
|
|
1220
1184
|
}
|
|
@@ -1232,7 +1196,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1232
1196
|
parsed: ParsedSelector,
|
|
1233
1197
|
displayMode: { hashLines: boolean; lineNumbers: boolean },
|
|
1234
1198
|
suffixResolution: { from: string; to: string } | undefined,
|
|
1235
|
-
repeatNotice: string | undefined,
|
|
1236
1199
|
signal: AbortSignal | undefined,
|
|
1237
1200
|
): Promise<{
|
|
1238
1201
|
outputText: string;
|
|
@@ -1252,7 +1215,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1252
1215
|
sourcePath: absolutePath,
|
|
1253
1216
|
entityLabel: "file",
|
|
1254
1217
|
raw: rawSelector,
|
|
1255
|
-
repeatNotice,
|
|
1256
1218
|
});
|
|
1257
1219
|
if (suffixResolution) {
|
|
1258
1220
|
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
@@ -1934,7 +1896,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1934
1896
|
let details: ReadToolDetails = {};
|
|
1935
1897
|
let sourcePath: string | undefined;
|
|
1936
1898
|
let columnTruncated = 0;
|
|
1937
|
-
let repeatNotice: string | undefined;
|
|
1938
1899
|
let truncationInfo:
|
|
1939
1900
|
| { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } }
|
|
1940
1901
|
| undefined;
|
|
@@ -1999,13 +1960,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1999
1960
|
}
|
|
2000
1961
|
} else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
|
|
2001
1962
|
const notebookText = await readEditableNotebookText(absolutePath, localReadPath);
|
|
2002
|
-
repeatNotice = this.#repeatReadNotice(absolutePath);
|
|
2003
1963
|
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
2004
1964
|
return this.#buildInMemoryMultiRangeResult(notebookText, parsed.ranges, {
|
|
2005
1965
|
details: { resolvedPath: absolutePath },
|
|
2006
1966
|
sourcePath: absolutePath,
|
|
2007
1967
|
entityLabel: "notebook",
|
|
2008
|
-
repeatNotice,
|
|
2009
1968
|
});
|
|
2010
1969
|
}
|
|
2011
1970
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
@@ -2013,13 +1972,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2013
1972
|
details: { resolvedPath: absolutePath },
|
|
2014
1973
|
sourcePath: absolutePath,
|
|
2015
1974
|
entityLabel: "notebook",
|
|
2016
|
-
repeatNotice,
|
|
2017
1975
|
});
|
|
2018
1976
|
} else if (shouldConvertWithMarkit) {
|
|
2019
1977
|
// Convert document via markit.
|
|
2020
1978
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
2021
1979
|
if (result.ok) {
|
|
2022
|
-
repeatNotice = this.#repeatReadNotice(absolutePath);
|
|
2023
1980
|
// Route the converted markdown through the in-memory text builder
|
|
2024
1981
|
// so line-range selectors (`file.pdf:50-100`, `:5-16,40-80`) and
|
|
2025
1982
|
// raw mode apply against the converted output. Without this,
|
|
@@ -2030,7 +1987,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2030
1987
|
details: { resolvedPath: absolutePath },
|
|
2031
1988
|
sourcePath: absolutePath,
|
|
2032
1989
|
entityLabel: "document",
|
|
2033
|
-
repeatNotice,
|
|
2034
1990
|
});
|
|
2035
1991
|
}
|
|
2036
1992
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
@@ -2039,7 +1995,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2039
1995
|
sourcePath: absolutePath,
|
|
2040
1996
|
entityLabel: "document",
|
|
2041
1997
|
raw: isRawSelector(parsed),
|
|
2042
|
-
repeatNotice,
|
|
2043
1998
|
});
|
|
2044
1999
|
} else if (result.error) {
|
|
2045
2000
|
content = [{ type: "text", text: `[Cannot read ${ext} file: ${result.error || "conversion failed"}]` }];
|
|
@@ -2047,7 +2002,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2047
2002
|
content = [{ type: "text", text: `[Cannot read ${ext} file: conversion failed]` }];
|
|
2048
2003
|
}
|
|
2049
2004
|
} else {
|
|
2050
|
-
repeatNotice = this.#repeatReadNotice(absolutePath);
|
|
2051
2005
|
if (
|
|
2052
2006
|
parsed.kind === "none" &&
|
|
2053
2007
|
this.session.settings.get("read.summarize.enabled") &&
|
|
@@ -2089,7 +2043,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2089
2043
|
parsed,
|
|
2090
2044
|
displayMode,
|
|
2091
2045
|
suffixResolution,
|
|
2092
|
-
repeatNotice,
|
|
2093
2046
|
undefined, // plain-file read: deterministic and fast, never abort mid-read
|
|
2094
2047
|
);
|
|
2095
2048
|
if (multiResult.bridgeResult) return multiResult.bridgeResult;
|
|
@@ -2113,7 +2066,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2113
2066
|
sourcePath: absolutePath,
|
|
2114
2067
|
entityLabel: "file",
|
|
2115
2068
|
raw: isRawSelector(parsed),
|
|
2116
|
-
repeatNotice,
|
|
2117
2069
|
});
|
|
2118
2070
|
if (suffixResolution) {
|
|
2119
2071
|
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
@@ -2415,14 +2367,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2415
2367
|
content = [{ type: "text", text: notice }, ...content];
|
|
2416
2368
|
}
|
|
2417
2369
|
}
|
|
2418
|
-
if (repeatNotice) {
|
|
2419
|
-
// Trailing nudge goes at the very end of the textual result so it never
|
|
2420
|
-
// disturbs hashline tag headers or inline notices.
|
|
2421
|
-
const lastText = content.findLast((c): c is TextContent => c.type === "text");
|
|
2422
|
-
if (lastText) {
|
|
2423
|
-
lastText.text = `${lastText.text}\n${repeatNotice}`;
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
2370
|
const resultBuilder = toolResult(details).content(content);
|
|
2427
2371
|
if (sourcePath) {
|
|
2428
2372
|
resultBuilder.sourcePath(sourcePath);
|
|
@@ -183,24 +183,32 @@ export function formatMoreItems(remaining: number, itemType: string): string {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* while the block is volatile. Keeping the live call window short avoids that
|
|
192
|
-
* across terminals without turning the transcript into an interactive scroller.
|
|
186
|
+
* Collapsed command/code previews render a tail window sized from the live
|
|
187
|
+
* viewport: terminal rows minus a reserve for the rest of the block (frame,
|
|
188
|
+
* Output section, stats line) and the editor/status area below the
|
|
189
|
+
* transcript. This keeps a volatile streaming block from growing past the
|
|
190
|
+
* viewport and stranding its top, while letting tall terminals show more.
|
|
193
191
|
*/
|
|
194
|
-
|
|
192
|
+
const PREVIEW_WINDOW_RESERVED_ROWS = 20;
|
|
193
|
+
/** Floor so tiny or unknown viewports still show a useful window. */
|
|
194
|
+
const PREVIEW_WINDOW_MIN_LINES = 6;
|
|
195
|
+
/** Assumed viewport when rows are unknown (non-TTY, tests). */
|
|
196
|
+
const PREVIEW_WINDOW_FALLBACK_ROWS = 30;
|
|
197
|
+
|
|
198
|
+
/** Tail-window height for collapsed command/code previews. */
|
|
199
|
+
export function previewWindowRows(): number {
|
|
200
|
+
const rows = process.stdout.rows || PREVIEW_WINDOW_FALLBACK_ROWS;
|
|
201
|
+
return Math.max(PREVIEW_WINDOW_MIN_LINES, rows - PREVIEW_WINDOW_RESERVED_ROWS);
|
|
202
|
+
}
|
|
195
203
|
|
|
196
204
|
/**
|
|
197
|
-
* Cap a pre-rendered
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
205
|
+
* Cap a pre-rendered command preview to a viewport-sized tail window: the end
|
|
206
|
+
* of the command stays visible (it is the live edge while args stream) behind
|
|
207
|
+
* an "… N earlier lines" marker on top. The same window applies while
|
|
208
|
+
* streaming and after completion so the block never jumps; only `expanded`
|
|
209
|
+
* (ctrl+o) uncaps it.
|
|
202
210
|
*
|
|
203
|
-
* `prefix` (raw, e.g. a dim tree gutter) is prepended to the
|
|
211
|
+
* `prefix` (raw, e.g. a dim tree gutter) is prepended to the marker line so
|
|
204
212
|
* nested previews stay aligned.
|
|
205
213
|
*/
|
|
206
214
|
export function capPreviewLines(
|
|
@@ -208,24 +216,14 @@ export function capPreviewLines(
|
|
|
208
216
|
theme: Theme,
|
|
209
217
|
options: { max?: number; expanded?: boolean; prefix?: string } = {},
|
|
210
218
|
): string[] {
|
|
211
|
-
|
|
219
|
+
if (options.expanded) return lines;
|
|
220
|
+
const max = options.max ?? previewWindowRows();
|
|
212
221
|
if (lines.length <= max) return lines;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
const bodyBudget = max - 1; // reserve one summary row
|
|
219
|
-
const headCount = Math.max(1, Math.ceil(bodyBudget / 2));
|
|
220
|
-
const tailCount = Math.max(1, bodyBudget - headCount);
|
|
221
|
-
const hidden = Math.max(0, lines.length - headCount - tailCount);
|
|
222
|
-
const hint = formatExpandHint(theme, options.expanded, true);
|
|
223
|
-
const moreLine = `${formatMoreItems(hidden, "line")}${hint ? ` ${hint}` : ""}`;
|
|
224
|
-
return [
|
|
225
|
-
...lines.slice(0, headCount),
|
|
226
|
-
`${options.prefix ?? ""}${theme.fg("dim", moreLine)}`,
|
|
227
|
-
...lines.slice(lines.length - tailCount),
|
|
228
|
-
];
|
|
222
|
+
const visible = max <= 1 ? [] : lines.slice(lines.length - (max - 1));
|
|
223
|
+
const hidden = lines.length - visible.length;
|
|
224
|
+
const hint = formatExpandHint(theme, false, true);
|
|
225
|
+
const marker = `… ${hidden} earlier ${pluralize("line", hidden)}${hint ? ` ${hint}` : ""}`;
|
|
226
|
+
return [`${options.prefix ?? ""}${theme.fg("dim", marker)}`, ...visible];
|
|
229
227
|
}
|
|
230
228
|
|
|
231
229
|
export function formatMeta(meta: string[], theme: Theme): string {
|
package/src/tools/ssh.ts
CHANGED
|
@@ -329,9 +329,9 @@ export const sshToolRenderer = {
|
|
|
329
329
|
state: "success",
|
|
330
330
|
sections: [
|
|
331
331
|
{
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
332
|
+
// Viewport-sized tail window in every state — streaming and final
|
|
333
|
+
// render identically; only ctrl+o uncaps.
|
|
334
|
+
lines: capPreviewLines(cmdLines, uiTheme, { expanded }),
|
|
335
335
|
},
|
|
336
336
|
{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
|
|
337
337
|
],
|
package/src/tools/tts.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Ported from NousResearch/hermes-agent (MIT) — tools/tts_tool.py L167-171, L896-959.
|
|
2
2
|
|
|
3
3
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import { type ApiKey, ProviderHttpError, withAuth } from "@oh-my-pi/pi-ai";
|
|
4
5
|
import * as z from "zod/v4";
|
|
5
6
|
import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
|
|
6
7
|
import { ohMyPiXAIUserAgent, resolveXAIHttpCredentials } from "../lib/xai-http";
|
|
@@ -96,27 +97,46 @@ export const ttsTool: CustomTool<typeof ttsSchema, TtsToolDetails> = {
|
|
|
96
97
|
const timeoutSignal = AbortSignal.timeout(60_000);
|
|
97
98
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
98
99
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"Content-Type": "application/json",
|
|
104
|
-
"User-Agent": ohMyPiXAIUserAgent(),
|
|
105
|
-
},
|
|
106
|
-
body: JSON.stringify(payload),
|
|
107
|
-
signal: combinedSignal,
|
|
100
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
101
|
+
const apiKey: ApiKey = ctx.modelRegistry.resolver(creds.provider, {
|
|
102
|
+
sessionId,
|
|
103
|
+
baseUrl: creds.baseURL,
|
|
108
104
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
105
|
+
|
|
106
|
+
let response: Response;
|
|
107
|
+
try {
|
|
108
|
+
response = await withAuth(
|
|
109
|
+
apiKey,
|
|
110
|
+
async key => {
|
|
111
|
+
const resp = await fetch(`${creds.baseURL}/tts`, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${key}`,
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
"User-Agent": ohMyPiXAIUserAgent(),
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify(payload),
|
|
119
|
+
signal: combinedSignal,
|
|
120
|
+
});
|
|
121
|
+
if (!resp.ok) {
|
|
122
|
+
const detail = await resp.text();
|
|
123
|
+
throw new ProviderHttpError(`xAI TTS failed (${resp.status}): ${detail.slice(0, 300)}`, resp.status, {
|
|
124
|
+
headers: resp.headers,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return resp;
|
|
128
|
+
},
|
|
129
|
+
{ signal: combinedSignal },
|
|
130
|
+
);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const status = (error as { status?: unknown }).status;
|
|
133
|
+
if (error instanceof Error && typeof status === "number") {
|
|
134
|
+
return {
|
|
135
|
+
isError: true,
|
|
136
|
+
content: [{ type: "text", text: error.message }],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
120
140
|
}
|
|
121
141
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
122
142
|
await Bun.write(outputPath, bytes);
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -125,6 +125,56 @@ async function readImageViaPowerShell(): Promise<ClipboardImage | null> {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// PowerShell one-liner that emits the clipboard text verbatim on stdout, or
|
|
129
|
+
// nothing when the clipboard holds no text. `[Console]::Out.Write` avoids the
|
|
130
|
+
// trailing newline Write-Output would add; output encoding is forced to UTF-8
|
|
131
|
+
// so non-ASCII text survives the interop boundary regardless of console
|
|
132
|
+
// codepage.
|
|
133
|
+
const POWERSHELL_TEXT_SCRIPT = `
|
|
134
|
+
$ErrorActionPreference = 'Stop'
|
|
135
|
+
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
136
|
+
[Console]::Out.Write([string](Get-Clipboard -Raw))
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Read clipboard text through Windows PowerShell — native win32 or the WSL
|
|
141
|
+
* host over interop.
|
|
142
|
+
*
|
|
143
|
+
* Same rationale as `readImageViaPowerShell`: under WSL, the WSLg Wayland
|
|
144
|
+
* clipboard only works when `wl-clipboard` happens to be installed in the
|
|
145
|
+
* distro, while `powershell.exe` is always reachable. Forcing UTF-8 output
|
|
146
|
+
* encoding keeps non-ASCII text intact regardless of the console codepage
|
|
147
|
+
* (the legacy win32 `Get-Clipboard` shell-out mangled it), and `Bun.spawn`
|
|
148
|
+
* keeps a cold PowerShell start off the TUI event loop.
|
|
149
|
+
*
|
|
150
|
+
* Returns null when the bridge fails (WSL callers fall through to
|
|
151
|
+
* wl-paste/xclip); an empty string is a successful "no text" read.
|
|
152
|
+
*/
|
|
153
|
+
async function readTextViaPowerShell(): Promise<string | null> {
|
|
154
|
+
try {
|
|
155
|
+
const proc = Bun.spawn(["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", POWERSHELL_TEXT_SCRIPT], {
|
|
156
|
+
stdout: "pipe",
|
|
157
|
+
stderr: "ignore",
|
|
158
|
+
stdin: "ignore",
|
|
159
|
+
});
|
|
160
|
+
const timer = setTimeout(() => proc.kill(), POWERSHELL_TIMEOUT_MS);
|
|
161
|
+
let stdout = "";
|
|
162
|
+
try {
|
|
163
|
+
stdout = await new Response(proc.stdout).text();
|
|
164
|
+
await proc.exited;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
logger.warn("clipboard: powershell text read failed", { error: String(err) });
|
|
167
|
+
return null;
|
|
168
|
+
} finally {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
}
|
|
171
|
+
if (proc.exitCode !== 0) return null;
|
|
172
|
+
return stdout.replaceAll("\r\n", "\n");
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
128
178
|
/**
|
|
129
179
|
* Read an image from the system clipboard.
|
|
130
180
|
*
|
|
@@ -165,14 +215,16 @@ export async function readTextFromClipboard(): Promise<string> {
|
|
|
165
215
|
return execSync("pbpaste", { encoding: "utf8", timeout: 2000 }).toString();
|
|
166
216
|
}
|
|
167
217
|
if (p === "win32") {
|
|
168
|
-
return
|
|
169
|
-
encoding: "utf8",
|
|
170
|
-
timeout: 2000,
|
|
171
|
-
}).toString();
|
|
218
|
+
return (await readTextViaPowerShell()) ?? "";
|
|
172
219
|
}
|
|
173
220
|
if (process.env.TERMUX_VERSION) {
|
|
174
221
|
return execSync("termux-clipboard-get", { encoding: "utf8", timeout: 2000 }).toString();
|
|
175
222
|
}
|
|
223
|
+
if (isWsl()) {
|
|
224
|
+
const text = await readTextViaPowerShell();
|
|
225
|
+
if (text !== null) return text;
|
|
226
|
+
// Bridge failed — fall through to the wl-paste/xclip paths below.
|
|
227
|
+
}
|
|
176
228
|
const hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY);
|
|
177
229
|
const hasX11Display = Boolean(process.env.DISPLAY);
|
|
178
230
|
if (hasWaylandDisplay) {
|
|
@@ -112,11 +112,7 @@ export async function generateCommitMessage(
|
|
|
112
112
|
messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
|
-
apiKey: registry.resolver(candidate.model
|
|
116
|
-
sessionId,
|
|
117
|
-
baseUrl: candidate.model.baseUrl,
|
|
118
|
-
modelId: candidate.model.id,
|
|
119
|
-
}),
|
|
115
|
+
apiKey: registry.resolver(candidate.model, sessionId),
|
|
120
116
|
maxTokens,
|
|
121
117
|
reasoning: toReasoningEffort(candidate.thinkingLevel),
|
|
122
118
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hslToHex, relativeLuminance } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { hexToHsv, hslToHex, relativeLuminance } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Derive a stable hue (0-359) from a string using djb2 hash.
|
|
@@ -25,23 +25,97 @@ function accentLuminanceCap(surfaceLuminance: number): number {
|
|
|
25
25
|
return Math.max(0, (surfaceLuminance + 0.05) / ACCENT_MIN_CONTRAST - 0.05);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Minimum angular distance in hue degrees from any theme color to avoid visual collision. */
|
|
29
|
+
const MIN_HUE_DISTANCE = 10;
|
|
30
|
+
/** Saturation threshold below which hue is meaningless (near-gray). */
|
|
31
|
+
const MIN_SATURATION_FOR_HUE = 0.1;
|
|
32
|
+
|
|
33
|
+
/** Angular distance between two hue values (0-360). */
|
|
34
|
+
function hueDistance(a: number, b: number): number {
|
|
35
|
+
const d = Math.abs(a - b);
|
|
36
|
+
return Math.min(d, 360 - d);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse hue (0-360) from a hex color string.
|
|
41
|
+
* Returns undefined for near-gray colors where hue is not meaningful.
|
|
42
|
+
*/
|
|
43
|
+
function hexToHue(hex: string): number | undefined {
|
|
44
|
+
const hsv = hexToHsv(hex);
|
|
45
|
+
if (hsv.s < MIN_SATURATION_FOR_HUE) return undefined;
|
|
46
|
+
return hsv.h;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Find a hue at least {@link MIN_HUE_DISTANCE} from all occupied hues,
|
|
51
|
+
* clamped to [lo, hi] to prevent leaving the intended hue band.
|
|
52
|
+
* Returns `target` unchanged if no safe hue exists within bounds.
|
|
53
|
+
*/
|
|
54
|
+
function findSafeHue(target: number, occupied: number[], lo: number, hi: number): number {
|
|
55
|
+
if (occupied.length === 0) return target;
|
|
56
|
+
if (occupied.every(h => hueDistance(target, h) >= MIN_HUE_DISTANCE)) {
|
|
57
|
+
return target;
|
|
58
|
+
}
|
|
59
|
+
for (let d = 1; d <= hi - lo; d++) {
|
|
60
|
+
for (const dir of [1, -1]) {
|
|
61
|
+
const candidate = Math.max(lo, Math.min(hi, target + d * dir));
|
|
62
|
+
if (occupied.every(h => hueDistance(candidate, h) >= MIN_HUE_DISTANCE)) {
|
|
63
|
+
return candidate;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// fallback: keep the original target if no safe spot exists within the band
|
|
68
|
+
return target;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Hue range low and high for dark themes (warm: red → yellow → green). */
|
|
72
|
+
const DARK_HUE_START = 0;
|
|
73
|
+
const DARK_HUE_END = 120;
|
|
74
|
+
/** Hue range low and high for light themes (cool: cyan → blue → purple). */
|
|
75
|
+
const LIGHT_HUE_START = 180;
|
|
76
|
+
const LIGHT_HUE_END = 300;
|
|
77
|
+
|
|
28
78
|
/**
|
|
29
|
-
* Derive a stable CSS hex accent color from a session name.
|
|
79
|
+
* Derive a stable CSS hex accent color from a session name and the active theme.
|
|
80
|
+
*
|
|
81
|
+
* Picks a hue from a **dark/light-specific range** so the accent feels natural
|
|
82
|
+
* for the theme type (warm on dark, cool on light). The session name hash
|
|
83
|
+
* determines the exact hue within the range. The result is checked against
|
|
84
|
+
* all theme color hues and shifted if it lands within {@link MIN_HUE_DISTANCE}
|
|
85
|
+
* of an existing theme hue, but is clamped to the hue band so it never
|
|
86
|
+
* drifts into an unrelated part of the spectrum.
|
|
30
87
|
*
|
|
31
88
|
* On dark themes (`surfaceLuminance` undefined) the accent is vivid (high
|
|
32
89
|
* saturation, high lightness). On light themes the lightness is reduced until the
|
|
33
90
|
* accent's perceived luminance clears {@link ACCENT_MIN_CONTRAST} against the
|
|
34
91
|
* actual surface it renders on — so it stays legible on near-white *and* mid-light
|
|
35
|
-
* backgrounds
|
|
92
|
+
* backgrounds.
|
|
93
|
+
*
|
|
94
|
+
* @param name — session name for per-session uniqueness.
|
|
95
|
+
* @param themeColorHexes — all theme colors to check collision against.
|
|
96
|
+
* @param surfaceLuminance — undefined on dark themes; WCAG luminance of the
|
|
97
|
+
* status-line background on light themes.
|
|
36
98
|
*/
|
|
37
|
-
export function getSessionAccentHex(name: string, surfaceLuminance?: number): string {
|
|
38
|
-
|
|
99
|
+
export function getSessionAccentHex(name: string, themeColorHexes: string[], surfaceLuminance?: number): string {
|
|
100
|
+
// 1. Pick hue range based on theme mode
|
|
101
|
+
const hueStart = surfaceLuminance === undefined ? DARK_HUE_START : LIGHT_HUE_START;
|
|
102
|
+
const hueEnd = surfaceLuminance === undefined ? DARK_HUE_END : LIGHT_HUE_END;
|
|
103
|
+
const range = hueEnd - hueStart;
|
|
104
|
+
|
|
105
|
+
// 2. Session name picks within the range
|
|
106
|
+
let targetHue = hueStart + (nameToHue(name) % range);
|
|
107
|
+
|
|
108
|
+
// 3. Shift away if too close to any theme color — stays within [hueStart, hueEnd]
|
|
109
|
+
const themeHues = themeColorHexes.map(hexToHue).filter((h): h is number => h !== undefined);
|
|
110
|
+
targetHue = findSafeHue(targetHue, themeHues, hueStart, hueEnd);
|
|
111
|
+
|
|
112
|
+
// 4. Lightness/contrast — vivid on dark, bisected for AA on light
|
|
39
113
|
if (surfaceLuminance === undefined) {
|
|
40
|
-
return hslToHex(
|
|
114
|
+
return hslToHex(targetHue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS);
|
|
41
115
|
}
|
|
42
116
|
|
|
43
117
|
const cap = accentLuminanceCap(surfaceLuminance);
|
|
44
|
-
const top = hslToHex(
|
|
118
|
+
const top = hslToHex(targetHue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS);
|
|
45
119
|
if ((relativeLuminance(top) ?? 0) <= cap) return top;
|
|
46
120
|
|
|
47
121
|
// Bisect lightness: `lo` always yields luminance <= cap, `hi` always above it.
|
|
@@ -49,13 +123,13 @@ export function getSessionAccentHex(name: string, surfaceLuminance?: number): st
|
|
|
49
123
|
let hi = ACCENT_DARK_LIGHTNESS;
|
|
50
124
|
for (let i = 0; i < 20; i++) {
|
|
51
125
|
const mid = (lo + hi) / 2;
|
|
52
|
-
if ((relativeLuminance(hslToHex(
|
|
126
|
+
if ((relativeLuminance(hslToHex(targetHue, ACCENT_SATURATION, mid)) ?? 0) > cap) {
|
|
53
127
|
hi = mid;
|
|
54
128
|
} else {
|
|
55
129
|
lo = mid;
|
|
56
130
|
}
|
|
57
131
|
}
|
|
58
|
-
return hslToHex(
|
|
132
|
+
return hslToHex(targetHue, ACCENT_SATURATION, lo);
|
|
59
133
|
}
|
|
60
134
|
|
|
61
135
|
/**
|
|
@@ -258,7 +258,7 @@ export async function generateTitleOnline(
|
|
|
258
258
|
tools: [setTitleTool],
|
|
259
259
|
},
|
|
260
260
|
{
|
|
261
|
-
apiKey: registry.resolver(model
|
|
261
|
+
apiKey: registry.resolver(model, sessionId),
|
|
262
262
|
maxTokens,
|
|
263
263
|
disableReasoning: true,
|
|
264
264
|
toolChoice: { type: "tool", name: SET_TITLE_TOOL_NAME },
|