@oh-my-pi/pi-coding-agent 15.11.3 → 15.11.6
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 +107 -0
- package/dist/cli.js +692 -607
- package/dist/types/cli/usage-cli.d.ts +10 -1
- package/dist/types/commands/usage.d.ts +9 -0
- 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 +508 -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/reset-usage-selector.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -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 +26 -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/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -0
- package/dist/types/modes/session-observer-registry.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +23 -3
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/context-usage.d.ts +6 -1
- package/dist/types/session/agent-session.d.ts +28 -8
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/codex-auto-reset.d.ts +107 -0
- package/dist/types/session/snapcompact-inline.d.ts +129 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
- package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
- package/dist/types/system-prompt.d.ts +3 -1
- package/dist/types/task/render.d.ts +17 -6
- package/dist/types/tools/gh.d.ts +3 -0
- package/dist/types/tools/render-utils.d.ts +8 -16
- package/dist/types/tools/todo.d.ts +0 -11
- 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/cli/usage-cli.ts +187 -16
- package/src/commands/usage.ts +8 -0
- 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 +654 -153
- package/src/config/settings.ts +9 -0
- 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 +6 -6
- 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/reset-usage-selector.ts +161 -0
- package/src/modes/components/session-selector.ts +8 -2
- package/src/modes/components/settings-defs.ts +19 -4
- package/src/modes/components/settings-selector.ts +510 -95
- 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 +87 -12
- package/src/modes/components/transcript-container.ts +49 -1
- package/src/modes/components/tree-selector.ts +16 -6
- package/src/modes/controllers/command-controller.ts +61 -8
- 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 +149 -61
- package/src/modes/interactive-mode.ts +63 -2
- package/src/modes/rpc/rpc-mode.ts +2 -1
- package/src/modes/session-observer-registry.ts +61 -3
- package/src/modes/shared.ts +2 -0
- package/src/modes/theme/theme.ts +102 -9
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/context-usage.ts +78 -2
- 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-context-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-context-stub.md +1 -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/browser.md +33 -43
- package/src/prompts/tools/eval.md +27 -50
- package/src/prompts/tools/irc.md +29 -31
- package/src/prompts/tools/read.md +31 -37
- package/src/prompts/tools/task.md +3 -3
- package/src/prompts/tools/todo.md +1 -2
- package/src/sdk.ts +23 -1
- package/src/session/agent-session.ts +221 -29
- package/src/session/auth-storage.ts +4 -0
- package/src/session/codex-auto-reset.ts +190 -0
- package/src/session/session-dump-format.ts +8 -1
- package/src/session/session-manager.ts +5 -5
- package/src/session/snapcompact-inline.ts +524 -0
- package/src/slash-commands/builtin-registry.ts +145 -8
- package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
- package/src/slash-commands/helpers/context-report.ts +28 -1
- package/src/slash-commands/helpers/reset-usage.ts +66 -0
- package/src/slash-commands/helpers/usage-report.ts +36 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/index.ts +30 -7
- package/src/task/render.ts +57 -32
- 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/todo.ts +8 -128
- 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/todo.ts
CHANGED
|
@@ -21,13 +21,6 @@ export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
|
|
|
21
21
|
export interface TodoItem {
|
|
22
22
|
content: string;
|
|
23
23
|
status: TodoStatus;
|
|
24
|
-
/**
|
|
25
|
-
* Append-only list of freeform notes attached by `op: "note"`.
|
|
26
|
-
* Each element is one note and may itself be multi-line.
|
|
27
|
-
* Rendered as text only when the task is in_progress; otherwise shown as a
|
|
28
|
-
* dim marker indicating the task has notes.
|
|
29
|
-
*/
|
|
30
|
-
notes?: string[];
|
|
31
24
|
}
|
|
32
25
|
|
|
33
26
|
export interface TodoPhase {
|
|
@@ -51,7 +44,7 @@ export interface TodoToolDetails {
|
|
|
51
44
|
// =============================================================================
|
|
52
45
|
|
|
53
46
|
const TodoOp = z
|
|
54
|
-
.enum(["init", "start", "done", "rm", "drop", "append", "
|
|
47
|
+
.enum(["init", "start", "done", "rm", "drop", "append", "view"] as const)
|
|
55
48
|
.describe("operation to apply");
|
|
56
49
|
|
|
57
50
|
const InitListEntry = z.object({
|
|
@@ -65,7 +58,6 @@ const TodoOpEntry = z.object({
|
|
|
65
58
|
task: z.string().optional().describe("task content"),
|
|
66
59
|
phase: z.string().optional().describe("phase name"),
|
|
67
60
|
items: z.array(z.string().describe("task content")).min(1).optional().describe("tasks to append"),
|
|
68
|
-
text: z.string().optional().describe("note text"),
|
|
69
61
|
});
|
|
70
62
|
|
|
71
63
|
const todoSchema = z
|
|
@@ -94,9 +86,7 @@ function findPhaseByName(phases: TodoPhase[], name: string): TodoPhase | undefin
|
|
|
94
86
|
}
|
|
95
87
|
|
|
96
88
|
function cloneTask(task: TodoItem): TodoItem {
|
|
97
|
-
|
|
98
|
-
if (task.notes && task.notes.length > 0) out.notes = [...task.notes];
|
|
99
|
-
return out;
|
|
89
|
+
return { content: task.content, status: task.status };
|
|
100
90
|
}
|
|
101
91
|
|
|
102
92
|
function clonePhases(phases: TodoPhase[]): TodoPhase[] {
|
|
@@ -392,17 +382,6 @@ function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string
|
|
|
392
382
|
}
|
|
393
383
|
case "rm":
|
|
394
384
|
return removeTasks(phases, entry, errors);
|
|
395
|
-
case "note": {
|
|
396
|
-
const hit = resolveTaskOrError(phases, entry.task, errors);
|
|
397
|
-
if (!hit) return phases;
|
|
398
|
-
const text = (entry.text ?? "").replace(/\s+$/u, "");
|
|
399
|
-
if (!text) {
|
|
400
|
-
errors.push("Missing text for note operation");
|
|
401
|
-
return phases;
|
|
402
|
-
}
|
|
403
|
-
hit.task.notes = hit.task.notes ? [...hit.task.notes, text] : [text];
|
|
404
|
-
return phases;
|
|
405
|
-
}
|
|
406
385
|
case "append":
|
|
407
386
|
return appendItems(phases, entry, errors);
|
|
408
387
|
case "view":
|
|
@@ -448,14 +427,6 @@ export function phasesToMarkdown(phases: TodoPhase[]): string {
|
|
|
448
427
|
out.push(`# ${phases[i].name}`);
|
|
449
428
|
for (const task of phases[i].tasks) {
|
|
450
429
|
out.push(`- [${STATUS_TO_MARKER[task.status]}] ${task.content}`);
|
|
451
|
-
if (task.notes && task.notes.length > 0) {
|
|
452
|
-
for (let j = 0; j < task.notes.length; j++) {
|
|
453
|
-
if (j > 0) out.push(" >");
|
|
454
|
-
for (const noteLine of task.notes[j].split("\n")) {
|
|
455
|
-
out.push(noteLine === "" ? " >" : ` > ${noteLine}`);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
430
|
}
|
|
460
431
|
}
|
|
461
432
|
return `${out.join("\n")}\n`;
|
|
@@ -477,45 +448,16 @@ export function markdownToPhases(md: string): { phases: TodoPhase[]; errors: str
|
|
|
477
448
|
const errors: string[] = [];
|
|
478
449
|
const phases: TodoPhase[] = [];
|
|
479
450
|
let currentPhase: TodoPhase | undefined;
|
|
480
|
-
let currentTask: TodoItem | undefined;
|
|
481
|
-
let noteBuf: string[] = [];
|
|
482
|
-
|
|
483
|
-
const flushNote = () => {
|
|
484
|
-
if (!currentTask || noteBuf.length === 0) {
|
|
485
|
-
noteBuf = [];
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
while (noteBuf.length > 0 && noteBuf[noteBuf.length - 1] === "") noteBuf.pop();
|
|
489
|
-
if (noteBuf.length === 0) return;
|
|
490
|
-
const joined = noteBuf.join("\n");
|
|
491
|
-
currentTask.notes = currentTask.notes ? [...currentTask.notes, joined] : [joined];
|
|
492
|
-
noteBuf = [];
|
|
493
|
-
};
|
|
494
451
|
|
|
495
452
|
const lines = md.split(/\r?\n/);
|
|
496
453
|
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
497
454
|
const raw = lines[lineNum];
|
|
498
455
|
|
|
499
|
-
// Blockquote line attached to the current task: ` > text` or ` >`
|
|
500
|
-
const noteMatch = /^\s*>\s?(.*)$/.exec(raw);
|
|
501
|
-
if (noteMatch && currentTask) {
|
|
502
|
-
const noteLine = noteMatch[1];
|
|
503
|
-
if (noteLine === "") {
|
|
504
|
-
// Blank `>` separates two distinct notes
|
|
505
|
-
flushNote();
|
|
506
|
-
} else {
|
|
507
|
-
noteBuf.push(noteLine);
|
|
508
|
-
}
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
456
|
const trimmed = raw.trim();
|
|
513
457
|
if (!trimmed) continue;
|
|
514
458
|
|
|
515
459
|
const headingMatch = /^#{1,6}\s+(.+?)\s*$/.exec(trimmed);
|
|
516
460
|
if (headingMatch) {
|
|
517
|
-
flushNote();
|
|
518
|
-
currentTask = undefined;
|
|
519
461
|
currentPhase = { name: headingMatch[1].trim(), tasks: [] };
|
|
520
462
|
phases.push(currentPhase);
|
|
521
463
|
continue;
|
|
@@ -523,7 +465,6 @@ export function markdownToPhases(md: string): { phases: TodoPhase[]; errors: str
|
|
|
523
465
|
|
|
524
466
|
const taskMatch = /^[-*+]\s*\[(.?)\]\s+(.+?)\s*$/.exec(trimmed);
|
|
525
467
|
if (taskMatch) {
|
|
526
|
-
flushNote();
|
|
527
468
|
if (!currentPhase) {
|
|
528
469
|
currentPhase = { name: "Todos", tasks: [] };
|
|
529
470
|
phases.push(currentPhase);
|
|
@@ -532,19 +473,14 @@ export function markdownToPhases(md: string): { phases: TodoPhase[]; errors: str
|
|
|
532
473
|
const status = MARKER_TO_STATUS[marker];
|
|
533
474
|
if (!status) {
|
|
534
475
|
errors.push(`Line ${lineNum + 1}: unknown status marker "[${marker}]" (use [ ], [x], [/], [-])`);
|
|
535
|
-
currentTask = undefined;
|
|
536
476
|
continue;
|
|
537
477
|
}
|
|
538
|
-
|
|
539
|
-
currentPhase.tasks.push(currentTask);
|
|
478
|
+
currentPhase.tasks.push({ content: taskMatch[2].trim(), status });
|
|
540
479
|
continue;
|
|
541
480
|
}
|
|
542
481
|
|
|
543
|
-
flushNote();
|
|
544
|
-
currentTask = undefined;
|
|
545
482
|
errors.push(`Line ${lineNum + 1}: unrecognized syntax "${trimmed}"`);
|
|
546
483
|
}
|
|
547
|
-
flushNote();
|
|
548
484
|
|
|
549
485
|
normalizeInProgressTask(phases);
|
|
550
486
|
return { phases, errors };
|
|
@@ -596,17 +532,7 @@ function formatSummary(phases: TodoPhase[], errors: string[], readOnly = false):
|
|
|
596
532
|
: task.status === "abandoned"
|
|
597
533
|
? "✗"
|
|
598
534
|
: "○";
|
|
599
|
-
|
|
600
|
-
const noteMarker = noteCount > 0 ? ` (+${noteCount} note${noteCount === 1 ? "" : "s"})` : "";
|
|
601
|
-
lines.push(` ${sym} ${task.content}${noteMarker}`);
|
|
602
|
-
if (task.status === "in_progress" && task.notes && task.notes.length > 0) {
|
|
603
|
-
for (let j = 0; j < task.notes.length; j++) {
|
|
604
|
-
if (j > 0) lines.push(" ---");
|
|
605
|
-
for (const noteLine of task.notes[j].split("\n")) {
|
|
606
|
-
lines.push(` ${noteLine}`);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
535
|
+
lines.push(` ${sym} ${task.content}`);
|
|
610
536
|
}
|
|
611
537
|
}
|
|
612
538
|
return lines.join("\n");
|
|
@@ -675,27 +601,6 @@ type TodoRenderArgs = {
|
|
|
675
601
|
}>;
|
|
676
602
|
};
|
|
677
603
|
|
|
678
|
-
const SUP_DIGITS: Record<string, string> = {
|
|
679
|
-
"0": "\u2070",
|
|
680
|
-
"1": "\u00b9",
|
|
681
|
-
"2": "\u00b2",
|
|
682
|
-
"3": "\u00b3",
|
|
683
|
-
"4": "\u2074",
|
|
684
|
-
"5": "\u2075",
|
|
685
|
-
"6": "\u2076",
|
|
686
|
-
"7": "\u2077",
|
|
687
|
-
"8": "\u2078",
|
|
688
|
-
"9": "\u2079",
|
|
689
|
-
};
|
|
690
|
-
|
|
691
|
-
function toSuperscript(n: number): string {
|
|
692
|
-
return n
|
|
693
|
-
.toString()
|
|
694
|
-
.split("")
|
|
695
|
-
.map(d => SUP_DIGITS[d] ?? d)
|
|
696
|
-
.join("");
|
|
697
|
-
}
|
|
698
|
-
|
|
699
604
|
// =============================================================================
|
|
700
605
|
// Phase numbering (display-only)
|
|
701
606
|
// =============================================================================
|
|
@@ -735,11 +640,6 @@ export function formatPhaseDisplayName(name: string, oneBasedIndex: number): str
|
|
|
735
640
|
return `${phaseRomanNumeral(oneBasedIndex)}. ${name}`;
|
|
736
641
|
}
|
|
737
642
|
|
|
738
|
-
function noteMarker(count: number, uiTheme: Theme): string {
|
|
739
|
-
if (count <= 0) return "";
|
|
740
|
-
return uiTheme.fg("dim", chalk.italic(` \u207a${toSuperscript(count)}`));
|
|
741
|
-
}
|
|
742
|
-
|
|
743
643
|
export const TODO_STRIKE_HOLD_FRAMES = 2;
|
|
744
644
|
export const TODO_STRIKE_REVEAL_FRAMES = 12;
|
|
745
645
|
export const TODO_STRIKE_TOTAL_FRAMES = TODO_STRIKE_HOLD_FRAMES + TODO_STRIKE_REVEAL_FRAMES;
|
|
@@ -775,7 +675,6 @@ function formatTodoLine(
|
|
|
775
675
|
frame: number | undefined,
|
|
776
676
|
): string {
|
|
777
677
|
const checkbox = uiTheme.checkbox;
|
|
778
|
-
const marker = noteMarker(item.notes?.length ?? 0, uiTheme);
|
|
779
678
|
switch (item.status) {
|
|
780
679
|
case "completed": {
|
|
781
680
|
const revealCount = completionKeys.has(item.content) ? strikeRevealCount(item.content, frame) : undefined;
|
|
@@ -783,33 +682,15 @@ function formatTodoLine(
|
|
|
783
682
|
revealCount === undefined
|
|
784
683
|
? strikethroughText(item.content)
|
|
785
684
|
: partialStrikethrough(item.content, revealCount);
|
|
786
|
-
return uiTheme.fg("success", `${prefix}${checkbox.checked} ${content}`)
|
|
685
|
+
return uiTheme.fg("success", `${prefix}${checkbox.checked} ${content}`);
|
|
787
686
|
}
|
|
788
687
|
case "in_progress":
|
|
789
|
-
return uiTheme.fg("accent", `${prefix}${checkbox.unchecked} ${item.content}`)
|
|
688
|
+
return uiTheme.fg("accent", `${prefix}${checkbox.unchecked} ${item.content}`);
|
|
790
689
|
case "abandoned":
|
|
791
|
-
return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${strikethroughText(item.content)}`)
|
|
690
|
+
return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${strikethroughText(item.content)}`);
|
|
792
691
|
default:
|
|
793
|
-
return uiTheme.fg("dim", `${prefix}${checkbox.unchecked} ${item.content}`)
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function renderNoteAttachments(phases: TodoPhase[], uiTheme: Theme, indent: string): string[] {
|
|
798
|
-
const lines: string[] = [];
|
|
799
|
-
for (const phase of phases) {
|
|
800
|
-
for (const task of phase.tasks) {
|
|
801
|
-
if (task.status !== "in_progress" || !task.notes || task.notes.length === 0) continue;
|
|
802
|
-
lines.push("");
|
|
803
|
-
lines.push(`${indent}${uiTheme.fg("dim", chalk.italic(`§ notes — ${task.content}`))}`);
|
|
804
|
-
for (let j = 0; j < task.notes.length; j++) {
|
|
805
|
-
if (j > 0) lines.push("");
|
|
806
|
-
for (const noteLine of task.notes[j].split("\n")) {
|
|
807
|
-
lines.push(`${indent} ${uiTheme.fg("dim", noteLine)}`);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
}
|
|
692
|
+
return uiTheme.fg("dim", `${prefix}${checkbox.unchecked} ${item.content}`);
|
|
811
693
|
}
|
|
812
|
-
return lines;
|
|
813
694
|
}
|
|
814
695
|
|
|
815
696
|
/**
|
|
@@ -966,7 +847,6 @@ export const todoToolRenderer = {
|
|
|
966
847
|
bodyLines.push(`${indent}${line}`);
|
|
967
848
|
}
|
|
968
849
|
}
|
|
969
|
-
bodyLines.push(...renderNoteAttachments(phases, uiTheme, indent));
|
|
970
850
|
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
971
851
|
return {
|
|
972
852
|
header,
|