@oh-my-pi/pi-coding-agent 15.7.3 → 15.7.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 +69 -1
- package/dist/types/config/settings-schema.d.ts +12 -22
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +24 -5
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/local-protocol.d.ts +19 -9
- package/dist/types/internal-urls/types.d.ts +14 -0
- package/dist/types/lsp/client.d.ts +3 -0
- package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +4 -0
- package/dist/types/mcp/manager.d.ts +14 -5
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/custom-editor.d.ts +0 -1
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/controllers/command-controller.d.ts +2 -3
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +10 -7
- package/dist/types/session/shake-types.d.ts +3 -3
- package/dist/types/task/repair-args.d.ts +52 -0
- package/dist/types/tiny/models.d.ts +0 -14
- package/dist/types/tiny/title-client.d.ts +28 -2
- package/dist/types/tiny/title-protocol.d.ts +8 -9
- package/dist/types/tools/ask.d.ts +8 -6
- package/dist/types/tools/eval-backends.d.ts +12 -0
- package/dist/types/tools/eval-render.d.ts +52 -0
- package/dist/types/tools/eval.d.ts +2 -35
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/index.d.ts +4 -11
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tui/output-block.d.ts +11 -10
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -0
- package/package.json +9 -9
- package/scripts/build-binary.ts +0 -1
- package/src/cli.ts +59 -0
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +13 -24
- package/src/config/settings.ts +10 -0
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +90 -0
- package/src/eval/__tests__/heartbeat.test.ts +84 -0
- package/src/eval/__tests__/llm-bridge.test.ts +30 -0
- package/src/eval/agent-bridge.ts +44 -38
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/exec/bash-executor.ts +31 -5
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +16 -18
- package/src/extensibility/extensions/runner.ts +22 -17
- package/src/extensibility/extensions/types.ts +39 -5
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/local-protocol.ts +23 -11
- package/src/internal-urls/types.ts +15 -0
- package/src/lsp/client.ts +28 -5
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/mcp/manager.ts +87 -4
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +9 -7
- package/src/modes/components/hook-selector.ts +159 -32
- package/src/modes/components/tool-execution.ts +20 -4
- package/src/modes/controllers/command-controller.ts +7 -39
- package/src/modes/controllers/event-controller.ts +38 -28
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +0 -15
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +17 -6
- package/src/modes/theme/theme-schema.json +30 -0
- package/src/modes/theme/theme.ts +39 -2
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/system/project-prompt.md +3 -2
- package/src/prompts/system/subagent-system-prompt.md +12 -8
- package/src/prompts/system/system-prompt.md +8 -6
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/session/agent-session.ts +75 -103
- package/src/session/shake-types.ts +4 -5
- package/src/slash-commands/builtin-registry.ts +2 -4
- package/src/task/executor.ts +14 -4
- package/src/task/index.ts +3 -2
- package/src/task/repair-args.ts +117 -0
- package/src/tiny/models.ts +0 -28
- package/src/tiny/title-client.ts +133 -43
- package/src/tiny/title-protocol.ts +11 -16
- package/src/tiny/worker.ts +6 -61
- package/src/tools/ask.ts +74 -32
- package/src/tools/ast-edit.ts +3 -0
- package/src/tools/ast-grep.ts +3 -0
- package/src/tools/eval-backends.ts +38 -0
- package/src/tools/eval-render.ts +750 -0
- package/src/tools/eval.ts +27 -754
- package/src/tools/find.ts +20 -6
- package/src/tools/gh.ts +1 -0
- package/src/tools/index.ts +7 -37
- package/src/tools/path-utils.ts +13 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/renderers.ts +1 -1
- package/src/tools/search.ts +12 -1
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +42 -79
- package/src/utils/git.ts +9 -3
package/src/tools/find.ts
CHANGED
|
@@ -192,7 +192,12 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
192
192
|
if (hasGlobPathChars(rawPattern)) {
|
|
193
193
|
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPattern}`);
|
|
194
194
|
}
|
|
195
|
-
const resource = await internalRouter.resolve(rawPattern
|
|
195
|
+
const resource = await internalRouter.resolve(rawPattern, {
|
|
196
|
+
cwd: this.session.cwd,
|
|
197
|
+
settings: this.session.settings,
|
|
198
|
+
signal,
|
|
199
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
200
|
+
});
|
|
196
201
|
if (!resource.sourcePath) {
|
|
197
202
|
throw new ToolError(`Cannot find internal URL without a backing file: ${rawPattern}`);
|
|
198
203
|
}
|
|
@@ -443,10 +448,14 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
443
448
|
// =============================================================================
|
|
444
449
|
|
|
445
450
|
interface FindRenderArgs {
|
|
446
|
-
paths?: string[];
|
|
451
|
+
paths?: string | string[];
|
|
447
452
|
limit?: number;
|
|
448
453
|
}
|
|
449
454
|
|
|
455
|
+
function formatFindRenderPaths(paths: FindRenderArgs["paths"]): string | undefined {
|
|
456
|
+
return Array.isArray(paths) ? paths.join(", ") : paths;
|
|
457
|
+
}
|
|
458
|
+
|
|
450
459
|
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
451
460
|
|
|
452
461
|
export const findToolRenderer = {
|
|
@@ -456,7 +465,7 @@ export const findToolRenderer = {
|
|
|
456
465
|
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
457
466
|
|
|
458
467
|
const text = renderStatusLine(
|
|
459
|
-
{ icon: "pending", title: "Find", description: args.paths
|
|
468
|
+
{ icon: "pending", title: "Find", description: formatFindRenderPaths(args.paths) || "*", meta },
|
|
460
469
|
uiTheme,
|
|
461
470
|
);
|
|
462
471
|
return new Text(text, 0, 0);
|
|
@@ -493,7 +502,7 @@ export const findToolRenderer = {
|
|
|
493
502
|
{
|
|
494
503
|
icon: "success",
|
|
495
504
|
title: "Find",
|
|
496
|
-
description: args?.paths
|
|
505
|
+
description: formatFindRenderPaths(args?.paths),
|
|
497
506
|
meta: [formatCount("file", lines.length)],
|
|
498
507
|
},
|
|
499
508
|
uiTheme,
|
|
@@ -528,7 +537,7 @@ export const findToolRenderer = {
|
|
|
528
537
|
|
|
529
538
|
if (fileCount === 0) {
|
|
530
539
|
const header = renderStatusLine(
|
|
531
|
-
{ icon: "warning", title: "Find", description: args?.paths
|
|
540
|
+
{ icon: "warning", title: "Find", description: formatFindRenderPaths(args?.paths), meta: ["0 files"] },
|
|
532
541
|
uiTheme,
|
|
533
542
|
);
|
|
534
543
|
const lines = [header, formatEmptyMessage("No files found", uiTheme)];
|
|
@@ -539,7 +548,12 @@ export const findToolRenderer = {
|
|
|
539
548
|
if (details?.scopePath) meta.push(`in ${details.scopePath}`);
|
|
540
549
|
if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
|
|
541
550
|
const header = renderStatusLine(
|
|
542
|
-
{
|
|
551
|
+
{
|
|
552
|
+
icon: truncated ? "warning" : "success",
|
|
553
|
+
title: "Find",
|
|
554
|
+
description: formatFindRenderPaths(args?.paths),
|
|
555
|
+
meta,
|
|
556
|
+
},
|
|
543
557
|
uiTheme,
|
|
544
558
|
);
|
|
545
559
|
|
package/src/tools/gh.ts
CHANGED
package/src/tools/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
2
2
|
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { ToolChoice } from "@oh-my-pi/pi-ai";
|
|
4
|
-
import {
|
|
4
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
6
6
|
import type { Settings } from "../config/settings";
|
|
7
7
|
import { EditTool } from "../edit";
|
|
@@ -34,6 +34,7 @@ import { BrowserTool } from "./browser";
|
|
|
34
34
|
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
|
35
35
|
import { DebugTool } from "./debug";
|
|
36
36
|
import { EvalTool } from "./eval";
|
|
37
|
+
import { resolveEvalBackends } from "./eval-backends";
|
|
37
38
|
import { FindTool } from "./find";
|
|
38
39
|
import { GithubTool } from "./gh";
|
|
39
40
|
import { InspectImageTool } from "./inspect-image";
|
|
@@ -74,6 +75,7 @@ export * from "./browser";
|
|
|
74
75
|
export * from "./checkpoint";
|
|
75
76
|
export * from "./debug";
|
|
76
77
|
export * from "./eval";
|
|
78
|
+
export * from "./eval-backends";
|
|
77
79
|
export * from "./find";
|
|
78
80
|
export * from "./gh";
|
|
79
81
|
export * from "./image-gen";
|
|
@@ -257,6 +259,10 @@ export interface ToolSession {
|
|
|
257
259
|
* by `getConflictHistory`. */
|
|
258
260
|
conflictHistory?: import("./conflict-detect").ConflictHistory;
|
|
259
261
|
|
|
262
|
+
/** Per-session ledger of post-edit LSP diagnostics already surfaced to the
|
|
263
|
+
* model for each file. Lazily initialized by `getDiagnosticsLedger`. */
|
|
264
|
+
diagnosticsLedger?: import("../lsp/diagnostics-ledger").DiagnosticsLedger;
|
|
265
|
+
|
|
260
266
|
/** Queue a hidden message to be injected at the next agent turn. */
|
|
261
267
|
queueDeferredMessage?(message: CustomMessage): void;
|
|
262
268
|
/** Get the active OpenTelemetry config so subagent dispatch can forward
|
|
@@ -331,42 +337,6 @@ export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
|
331
337
|
|
|
332
338
|
export type ToolName = keyof typeof BUILTIN_TOOLS;
|
|
333
339
|
|
|
334
|
-
export interface EvalBackendsAllowance {
|
|
335
|
-
python: boolean;
|
|
336
|
-
js: boolean;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Parse PI_PY / PI_JS environment variables. Each is a boolean flag; unset
|
|
341
|
-
* means "not specified, defer to settings". Returns null when neither is set
|
|
342
|
-
* so the caller can fall through to `readEvalBackendsAllowance` per key.
|
|
343
|
-
*/
|
|
344
|
-
function getEvalBackendsFromEnv(): EvalBackendsAllowance | null {
|
|
345
|
-
const pyEnv = $env.PI_PY;
|
|
346
|
-
const jsEnv = $env.PI_JS;
|
|
347
|
-
if (pyEnv === undefined && jsEnv === undefined) return null;
|
|
348
|
-
return {
|
|
349
|
-
python: pyEnv === undefined ? true : $flag("PI_PY"),
|
|
350
|
-
js: jsEnv === undefined ? true : $flag("PI_JS"),
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/** Read per-backend allowance from settings (defaults true). */
|
|
355
|
-
export function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance {
|
|
356
|
-
return {
|
|
357
|
-
python: session.settings.get("eval.py") ?? true,
|
|
358
|
-
js: session.settings.get("eval.js") ?? true,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Materialize the active eval backend allowance: PI_PY / PI_JS env flags
|
|
364
|
-
* override the per-key settings; otherwise settings (defaults true) win.
|
|
365
|
-
*/
|
|
366
|
-
export function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance {
|
|
367
|
-
return getEvalBackendsFromEnv() ?? readEvalBackendsAllowance(session);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
340
|
/**
|
|
371
341
|
* Create tools from BUILTIN_TOOLS registry.
|
|
372
342
|
*/
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
5
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
6
|
-
import { InternalUrlRouter } from "../internal-urls";
|
|
6
|
+
import { InternalUrlRouter, type LocalProtocolOptions } from "../internal-urls";
|
|
7
7
|
import { ToolError } from "./tool-errors";
|
|
8
8
|
|
|
9
9
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
@@ -740,6 +740,12 @@ export interface ToolScopeOptions {
|
|
|
740
740
|
surfaceExactFilePaths?: boolean;
|
|
741
741
|
/** Extra hint appended to "Path not found" when stat fails and the user supplied multiple paths. */
|
|
742
742
|
multipathStatHint?: string;
|
|
743
|
+
/** Calling session's settings — forwarded to the internal-URL router so caller-aware handlers (issue://, pr://) honor it. */
|
|
744
|
+
settings?: unknown;
|
|
745
|
+
/** Caller's abort signal — forwarded to the internal-URL router. */
|
|
746
|
+
signal?: AbortSignal;
|
|
747
|
+
/** Calling session's `local://` root mapping — pins resolutions to the calling session. */
|
|
748
|
+
localProtocolOptions?: LocalProtocolOptions;
|
|
743
749
|
}
|
|
744
750
|
|
|
745
751
|
export interface ToolScopeResolution {
|
|
@@ -778,7 +784,12 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
778
784
|
if (hasGlobPathChars(rawPath)) {
|
|
779
785
|
throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
|
|
780
786
|
}
|
|
781
|
-
const resource = await internalRouter.resolve(rawPath
|
|
787
|
+
const resource = await internalRouter.resolve(rawPath, {
|
|
788
|
+
cwd,
|
|
789
|
+
settings: opts.settings,
|
|
790
|
+
signal: opts.signal,
|
|
791
|
+
localProtocolOptions: opts.localProtocolOptions,
|
|
792
|
+
});
|
|
782
793
|
if (!resource.sourcePath) {
|
|
783
794
|
throw new ToolError(`Cannot ${internalUrlAction} internal URL without a backing file: ${rawPath}`);
|
|
784
795
|
}
|
package/src/tools/read.ts
CHANGED
|
@@ -2141,6 +2141,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2141
2141
|
cwd: this.session.cwd,
|
|
2142
2142
|
settings: this.session.settings,
|
|
2143
2143
|
signal,
|
|
2144
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
2144
2145
|
});
|
|
2145
2146
|
const details: ReadToolDetails = { resolvedPath: resource.sourcePath, contentType: resource.contentType };
|
|
2146
2147
|
|
package/src/tools/renderers.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { astGrepToolRenderer } from "./ast-grep";
|
|
|
17
17
|
import { bashToolRenderer } from "./bash";
|
|
18
18
|
import { browserToolRenderer } from "./browser/render";
|
|
19
19
|
import { debugToolRenderer } from "./debug";
|
|
20
|
-
import { evalToolRenderer } from "./eval";
|
|
20
|
+
import { evalToolRenderer } from "./eval-render";
|
|
21
21
|
import { findToolRenderer } from "./find";
|
|
22
22
|
import { githubToolRenderer } from "./gh-renderer";
|
|
23
23
|
import { inspectImageToolRenderer } from "./inspect-image-renderer";
|
package/src/tools/search.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
10
10
|
import * as z from "zod/v4";
|
|
11
11
|
import { recordFileSnapshot } from "../edit/file-snapshot-store";
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
|
+
import type { LocalProtocolOptions } from "../internal-urls/local-protocol";
|
|
13
14
|
import { InternalUrlRouter } from "../internal-urls/router";
|
|
14
15
|
import type { InternalResource, ResolveContext } from "../internal-urls/types";
|
|
15
16
|
import type { Theme } from "../modes/theme/theme";
|
|
@@ -543,6 +544,7 @@ async function resolveInternalSearchInputs(opts: {
|
|
|
543
544
|
settings: unknown;
|
|
544
545
|
signal?: AbortSignal;
|
|
545
546
|
archiveDisplayMap: ReadonlyMap<string, string>;
|
|
547
|
+
localProtocolOptions?: LocalProtocolOptions;
|
|
546
548
|
}): Promise<InternalSearchInputResolution> {
|
|
547
549
|
const internalRouter = InternalUrlRouter.instance();
|
|
548
550
|
const paths = opts.resolvedPaths.slice();
|
|
@@ -551,7 +553,12 @@ async function resolveInternalSearchInputs(opts: {
|
|
|
551
553
|
const virtualInputIndexes = new Set<number>();
|
|
552
554
|
const immutableSourcePaths = new Set<string>();
|
|
553
555
|
let virtualScopePath: string | undefined;
|
|
554
|
-
const context: ResolveContext = {
|
|
556
|
+
const context: ResolveContext = {
|
|
557
|
+
cwd: opts.cwd,
|
|
558
|
+
settings: opts.settings,
|
|
559
|
+
signal: opts.signal,
|
|
560
|
+
localProtocolOptions: opts.localProtocolOptions,
|
|
561
|
+
};
|
|
555
562
|
|
|
556
563
|
for (let idx = 0; idx < paths.length; idx++) {
|
|
557
564
|
const rawPath = paths[idx];
|
|
@@ -674,6 +681,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
674
681
|
settings: this.session.settings,
|
|
675
682
|
signal,
|
|
676
683
|
archiveDisplayMap,
|
|
684
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
677
685
|
});
|
|
678
686
|
const searchablePaths = internalResolution.paths;
|
|
679
687
|
const { virtualResources, virtualPathSet, virtualInputIndexes } = internalResolution;
|
|
@@ -738,6 +746,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
738
746
|
trackImmutableSources: true,
|
|
739
747
|
surfaceExactFilePaths: true,
|
|
740
748
|
multipathStatHint: " (`paths` entries must each exist relative to cwd)",
|
|
749
|
+
settings: this.session.settings,
|
|
750
|
+
signal,
|
|
751
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
741
752
|
});
|
|
742
753
|
searchPath = scope.searchPath;
|
|
743
754
|
isDirectory = scope.isDirectory;
|
package/src/tools/write.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
15
15
|
import { InternalUrlRouter } from "../internal-urls";
|
|
16
16
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
17
17
|
import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
|
|
18
|
+
import { getDiagnosticsLedger } from "../lsp/diagnostics-ledger";
|
|
18
19
|
import { getLanguageFromPath, highlightCode, type Theme } from "../modes/theme/theme";
|
|
19
20
|
import writeDescription from "../prompts/tools/write.md" with { type: "text" };
|
|
20
21
|
import type { ToolSession } from "../sdk";
|
|
@@ -278,8 +279,15 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
278
279
|
const enableLsp = session.enableLsp ?? true;
|
|
279
280
|
const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
|
|
280
281
|
const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnWrite");
|
|
282
|
+
const dedup = enableDiagnostics && session.settings.get("lsp.diagnosticsDeduplicate");
|
|
281
283
|
this.#writethrough = enableLsp
|
|
282
|
-
? createLspWritethrough(session.cwd, {
|
|
284
|
+
? createLspWritethrough(session.cwd, {
|
|
285
|
+
enableFormat,
|
|
286
|
+
enableDiagnostics,
|
|
287
|
+
transformDiagnostics: dedup
|
|
288
|
+
? (path, result) => getDiagnosticsLedger(session).reduce(path, result)
|
|
289
|
+
: undefined,
|
|
290
|
+
})
|
|
283
291
|
: writethroughNoop;
|
|
284
292
|
this.description = prompt.render(writeDescription);
|
|
285
293
|
}
|
package/src/tui/output-block.ts
CHANGED
|
@@ -19,55 +19,43 @@ export interface OutputBlockOptions {
|
|
|
19
19
|
animate?: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const BORDER_SHIMMER_TICK_MS =
|
|
23
|
-
/** Duration of one full
|
|
24
|
-
*
|
|
25
|
-
* instead of teleporting it. */
|
|
26
|
-
const
|
|
22
|
+
const BORDER_SHIMMER_TICK_MS = 16;
|
|
23
|
+
/** Duration of one full left↔right↔left bounce of the bottom-edge segment, in
|
|
24
|
+
* ms. Position is derived from the wall clock against this fixed cycle so a
|
|
25
|
+
* resize only nudges the segment proportionally instead of teleporting it. */
|
|
26
|
+
const BORDER_BOUNCE_MS = 3000;
|
|
27
27
|
/** Length, in border cells, of the moving segment. */
|
|
28
28
|
const BORDER_SEGMENT_LEN = 8;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Monotonic frame counter for animated borders
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Monotonic frame counter for animated borders, quantized to the TUI's ~16ms
|
|
32
|
+
* render cap so the cache key advances once per ~60fps frame — fine enough for a
|
|
33
|
+
* smooth segment sweep, coarse enough to coalesce multiple render passes that
|
|
34
|
+
* land inside the same frame.
|
|
34
35
|
*/
|
|
35
36
|
export function borderShimmerTick(): number {
|
|
36
37
|
return Math.floor(Date.now() / BORDER_SHIMMER_TICK_MS);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
/** Ease-in-out so the segment decelerates into and accelerates out of
|
|
40
|
+
/** Ease-in-out so the segment decelerates into and accelerates out of each wall. */
|
|
40
41
|
function easeInOutQuad(t: number): number {
|
|
41
42
|
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* from the wall clock against a fixed
|
|
50
|
-
*
|
|
46
|
+
* Column of the travelling segment's center on the bottom edge for a box of
|
|
47
|
+
* inner width `W` at time `now`. The segment bounces left → right → left across
|
|
48
|
+
* the bottom border: a triangle wave over one full there-and-back cycle, eased
|
|
49
|
+
* per leg so it slows as it nears each wall before reversing. Position is
|
|
50
|
+
* derived from the wall clock against a fixed cycle, so a resize shifts the
|
|
51
|
+
* center proportionally — no reset.
|
|
51
52
|
*/
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
let acc = 0;
|
|
59
|
-
let start = 0;
|
|
60
|
-
for (let i = 0; i < 4; i++) {
|
|
61
|
-
const len = edgeLens[i]!;
|
|
62
|
-
const frac = len / P;
|
|
63
|
-
if (len > 0 && t < acc + frac) {
|
|
64
|
-
const lf = (t - acc) / frac;
|
|
65
|
-
return (start + Math.floor(easeInOutQuad(lf) * len)) % P;
|
|
66
|
-
}
|
|
67
|
-
acc += frac;
|
|
68
|
-
start += len;
|
|
69
|
-
}
|
|
70
|
-
return P - 1;
|
|
53
|
+
export function borderSegmentHeadCol(W: number, now: number): number {
|
|
54
|
+
if (W <= 1) return 0;
|
|
55
|
+
const phase = (((now % BORDER_BOUNCE_MS) + BORDER_BOUNCE_MS) % BORDER_BOUNCE_MS) / BORDER_BOUNCE_MS;
|
|
56
|
+
// Triangle: 0→1 rightward over the first half, 1→0 leftward over the second.
|
|
57
|
+
const leg = phase < 0.5 ? phase * 2 : 2 - phase * 2;
|
|
58
|
+
return easeInOutQuad(leg) * (W - 1);
|
|
71
59
|
}
|
|
72
60
|
|
|
73
61
|
/**
|
|
@@ -164,30 +152,26 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
164
152
|
const W = lineWidth;
|
|
165
153
|
const animate = (options.animate ?? false) && (state === "running" || state === "pending") && W >= 2 && H >= 2;
|
|
166
154
|
|
|
167
|
-
// ── Segment geometry: one dark run
|
|
168
|
-
// top
|
|
169
|
-
|
|
170
|
-
const segLen = Math.min(BORDER_SEGMENT_LEN,
|
|
171
|
-
const head = animate ?
|
|
155
|
+
// ── Segment geometry: one dark run bounces left ↔ right along the bottom
|
|
156
|
+
// edge only. The top, interior separators, and side borders stay the flat
|
|
157
|
+
// accent color. ──
|
|
158
|
+
const segLen = animate ? Math.min(BORDER_SEGMENT_LEN, W) : 0;
|
|
159
|
+
const head = animate ? borderSegmentHeadCol(W, Date.now()) : 0;
|
|
160
|
+
const segHalf = segLen / 2;
|
|
172
161
|
const segAnsi = animate ? (darkenFgAnsi(theme.getFgAnsi(borderColor), 0.4) ?? theme.getFgAnsi("borderMuted")) : "";
|
|
173
162
|
const seg = (text: string) => `${segAnsi}${text}\x1b[39m`;
|
|
174
163
|
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (r === H - 1) return W - 1 + (H - 1) + (W - 1 - c);
|
|
180
|
-
return W - 1 + (H - 1) + (W - 1) + (H - 1 - r);
|
|
181
|
-
};
|
|
182
|
-
const isLit = (idx: number): boolean => (((idx - head) % P) + P) % P < segLen;
|
|
183
|
-
// Color a run of border glyphs starting at (row r, col startCol), grouping
|
|
164
|
+
// A bottom-edge column is lit when it lies within half a segment of the
|
|
165
|
+
// travelling center.
|
|
166
|
+
const isLit = (col: number): boolean => Math.abs(col - head) < segHalf;
|
|
167
|
+
// Color a run of bottom-edge glyphs starting at column `startCol`, grouping
|
|
184
168
|
// consecutive same-state cells so each run emits a single escape pair.
|
|
185
|
-
const colorEdge = (glyphs: string,
|
|
169
|
+
const colorEdge = (glyphs: string, startCol: number): string => {
|
|
186
170
|
let out = "";
|
|
187
171
|
let runLit: boolean | null = null;
|
|
188
172
|
let buf = "";
|
|
189
173
|
for (let i = 0; i < glyphs.length; i++) {
|
|
190
|
-
const lit = isLit(
|
|
174
|
+
const lit = isLit(startCol + i);
|
|
191
175
|
if (lit !== runLit) {
|
|
192
176
|
if (runLit !== null) out += (runLit ? seg : border)(buf);
|
|
193
177
|
buf = "";
|
|
@@ -199,10 +183,7 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
199
183
|
return out;
|
|
200
184
|
};
|
|
201
185
|
|
|
202
|
-
const renderBar = (
|
|
203
|
-
row: { leftChar: string; rightChar: string; label?: string; meta?: string },
|
|
204
|
-
r: number,
|
|
205
|
-
): string => {
|
|
186
|
+
const renderBar = (row: { leftChar: string; rightChar: string; label?: string; meta?: string }): string => {
|
|
206
187
|
const leftGlyphs = `${row.leftChar}${cap}`;
|
|
207
188
|
const rightGlyph = row.rightChar;
|
|
208
189
|
if (lineWidth <= 0) return border(leftGlyphs) + border(rightGlyph);
|
|
@@ -215,36 +196,22 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
215
196
|
const labelWidth = visibleWidth(trimmedLabel);
|
|
216
197
|
const fillCount = Math.max(0, lineWidth - leftWidth - labelWidth - rightWidth);
|
|
217
198
|
const fillGlyphs = h.repeat(fillCount);
|
|
218
|
-
|
|
219
|
-
return `${border(leftGlyphs)}${trimmedLabel}${border(fillGlyphs)}${border(rightGlyph)}`;
|
|
220
|
-
}
|
|
221
|
-
if (r === 0 || r === H - 1) {
|
|
222
|
-
// Top/bottom edge: the whole horizontal run lies on the perimeter.
|
|
223
|
-
const leftStr = colorEdge(leftGlyphs, r, 0);
|
|
224
|
-
const fillStr = colorEdge(fillGlyphs, r, leftWidth + labelWidth);
|
|
225
|
-
const rightStr = colorEdge(rightGlyph, r, lineWidth - rightWidth);
|
|
226
|
-
return `${leftStr}${trimmedLabel}${fillStr}${rightStr}`;
|
|
227
|
-
}
|
|
228
|
-
// Interior separator: only the first/last cell sit on the outer edge.
|
|
229
|
-
return `${colorEdge(row.leftChar, r, 0)}${border(cap)}${trimmedLabel}${border(fillGlyphs)}${colorEdge(rightGlyph, r, lineWidth - rightWidth)}`;
|
|
199
|
+
return `${border(leftGlyphs)}${trimmedLabel}${border(fillGlyphs)}${border(rightGlyph)}`;
|
|
230
200
|
};
|
|
231
201
|
|
|
232
|
-
const renderBottom = (row: { leftChar: string; rightChar: string }
|
|
202
|
+
const renderBottom = (row: { leftChar: string; rightChar: string }): string => {
|
|
233
203
|
const leftGlyphs = `${row.leftChar}${cap}`;
|
|
234
204
|
const rightGlyph = row.rightChar;
|
|
235
205
|
const fillCount = Math.max(0, lineWidth - visibleWidth(leftGlyphs) - visibleWidth(rightGlyph));
|
|
236
206
|
const fillGlyphs = h.repeat(fillCount);
|
|
237
207
|
if (!animate) return `${border(leftGlyphs)}${border(fillGlyphs)}${border(rightGlyph)}`;
|
|
238
|
-
const leftStr = colorEdge(leftGlyphs,
|
|
239
|
-
const fillStr = colorEdge(fillGlyphs,
|
|
240
|
-
const rightStr = colorEdge(rightGlyph,
|
|
208
|
+
const leftStr = colorEdge(leftGlyphs, 0);
|
|
209
|
+
const fillStr = colorEdge(fillGlyphs, visibleWidth(leftGlyphs));
|
|
210
|
+
const rightStr = colorEdge(rightGlyph, lineWidth - visibleWidth(rightGlyph));
|
|
241
211
|
return `${leftStr}${fillStr}${rightStr}`;
|
|
242
212
|
};
|
|
243
213
|
|
|
244
|
-
const renderContent = (inner: string
|
|
245
|
-
if (!animate) return `${border(`${v} `)}${inner}${border(v)}`;
|
|
246
|
-
return `${colorEdge(v, r, 0)} ${inner}${colorEdge(v, r, lineWidth - 1)}`;
|
|
247
|
-
};
|
|
214
|
+
const renderContent = (inner: string): string => `${border(`${v} `)}${inner}${border(v)}`;
|
|
248
215
|
|
|
249
216
|
const lines: string[] = [];
|
|
250
217
|
for (let r = 0; r < H; r++) {
|
|
@@ -254,11 +221,7 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
254
221
|
continue;
|
|
255
222
|
}
|
|
256
223
|
const line =
|
|
257
|
-
row.kind === "bar"
|
|
258
|
-
? renderBar(row, r)
|
|
259
|
-
: row.kind === "bottom"
|
|
260
|
-
? renderBottom(row, r)
|
|
261
|
-
: renderContent(row.inner, r);
|
|
224
|
+
row.kind === "bar" ? renderBar(row) : row.kind === "bottom" ? renderBottom(row) : renderContent(row.inner);
|
|
262
225
|
lines.push(padToWidth(line, lineWidth, bgFn));
|
|
263
226
|
}
|
|
264
227
|
|
package/src/utils/git.ts
CHANGED
|
@@ -1261,9 +1261,15 @@ export async function clone(url: string, targetDir: string, options: CloneOption
|
|
|
1261
1261
|
const absoluteTarget = path.resolve(targetDir);
|
|
1262
1262
|
await fs.promises.mkdir(path.dirname(absoluteTarget), { recursive: true });
|
|
1263
1263
|
|
|
1264
|
-
|
|
1264
|
+
// `git clone --depth 1 --single-branch` only fetches the tip of the target
|
|
1265
|
+
// branch, so any subsequent `git checkout <sha>` for a non-tip commit fails
|
|
1266
|
+
// with "reference is not a tree". When the caller pinned a specific SHA we
|
|
1267
|
+
// fall back to a full clone so the object is guaranteed to be present.
|
|
1268
|
+
const shallow = !options.sha;
|
|
1269
|
+
const args = ["clone"];
|
|
1270
|
+
if (shallow) args.push("--depth", "1");
|
|
1265
1271
|
if (options.ref) args.push("--branch", options.ref, "--single-branch");
|
|
1266
|
-
else args.push("--single-branch");
|
|
1272
|
+
else if (shallow) args.push("--single-branch");
|
|
1267
1273
|
args.push(url, absoluteTarget);
|
|
1268
1274
|
|
|
1269
1275
|
try {
|
|
@@ -1273,7 +1279,7 @@ export async function clone(url: string, targetDir: string, options: CloneOption
|
|
|
1273
1279
|
await checkout(absoluteTarget, options.sha, options.signal);
|
|
1274
1280
|
} catch {
|
|
1275
1281
|
await fs.promises.rm(absoluteTarget, { force: true, recursive: true });
|
|
1276
|
-
throw new Error(`Failed to checkout SHA ${options.sha}
|
|
1282
|
+
throw new Error(`Failed to checkout SHA ${options.sha} in cloned repository ${url}`);
|
|
1277
1283
|
}
|
|
1278
1284
|
}
|
|
1279
1285
|
} catch (err) {
|