@oh-my-pi/pi-coding-agent 17.1.6 → 17.1.8
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 +58 -0
- package/dist/{CHANGELOG-x9zt79k8.md → CHANGELOG-vt8ene9g.md} +58 -0
- package/dist/cli.js +4309 -7287
- package/dist/template-dys3vk5b.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +0 -9
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +16 -1
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -3
- package/dist/types/session/agent-session-types.d.ts +5 -5
- package/dist/types/session/agent-session.d.ts +26 -1
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/model-controls.d.ts +4 -4
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/turn-recovery.d.ts +1 -1
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +0 -12
- package/src/async/job-manager.ts +30 -7
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +2 -1
- package/src/commands/cleanse.ts +45 -0
- package/src/config/settings-schema.ts +15 -1
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/export/html/index.ts +17 -10
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/launch/broker.ts +14 -4
- package/src/modes/acp/acp-agent.ts +12 -9
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +36 -47
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/goal.md +1 -1
- package/src/sdk.ts +47 -50
- package/src/session/agent-session-types.ts +5 -5
- package/src/session/agent-session.ts +151 -11
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/model-controls.ts +9 -9
- package/src/session/session-advisors.ts +2 -7
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +158 -52
- package/src/session/streaming-output.ts +18 -6
- package/src/session/turn-recovery.ts +4 -4
- package/src/slash-commands/builtin-registry.ts +74 -2
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/worktree.ts +8 -0
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/bash.ts +16 -9
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +2 -2
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/index.ts +46 -17
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +87 -13
- package/src/tools/write.ts +51 -7
- package/src/tools/xdev.ts +198 -210
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/render.ts +2 -2
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
|
@@ -53,12 +53,17 @@ export interface OutputSummary {
|
|
|
53
53
|
export interface OutputSinkOptions {
|
|
54
54
|
artifactPath?: string;
|
|
55
55
|
artifactId?: string;
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Total inline body budget (bytes). Default DEFAULT_MAX_BYTES. The head
|
|
58
|
+
* window and rolling tail window share this budget, so a composed
|
|
59
|
+
* `dump()` body never exceeds it (plus the elision marker).
|
|
60
|
+
*/
|
|
57
61
|
spillThreshold?: number;
|
|
58
62
|
/**
|
|
59
63
|
* When > 0, the sink keeps the first `headBytes` of output in addition to
|
|
60
64
|
* the rolling tail window. Output between the two windows is elided
|
|
61
|
-
* (middle elision).
|
|
65
|
+
* (middle elision). Clamped to `spillThreshold / 2` so the tail keeps at
|
|
66
|
+
* least half the inline budget. Default 0 = tail-only behavior.
|
|
62
67
|
*/
|
|
63
68
|
headBytes?: number;
|
|
64
69
|
/**
|
|
@@ -799,7 +804,7 @@ export class OutputSink {
|
|
|
799
804
|
this.#artifactPath = artifactPath;
|
|
800
805
|
this.#artifactId = artifactId;
|
|
801
806
|
this.#spillThreshold = spillThreshold;
|
|
802
|
-
this.#headLimit = Math.max(0, headBytes);
|
|
807
|
+
this.#headLimit = Math.max(0, Math.min(headBytes, Math.floor(spillThreshold / 2)));
|
|
803
808
|
this.#maxColumns = Math.max(0, maxColumns);
|
|
804
809
|
this.#onChunk = onChunk;
|
|
805
810
|
this.#chunkThrottleMs = chunkThrottleMs;
|
|
@@ -969,16 +974,23 @@ export class OutputSink {
|
|
|
969
974
|
return parts.join("");
|
|
970
975
|
}
|
|
971
976
|
|
|
977
|
+
// The rolling tail budget is whatever the head window has not consumed of
|
|
978
|
+
// the inline budget: `spillThreshold - #headBytes`. While the head window
|
|
979
|
+
// fills it shrinks toward `spillThreshold - headLimit`; after `replace()`
|
|
980
|
+
// (head cleared) it grows back to the full threshold. This keeps
|
|
981
|
+
// `head + tail <= spillThreshold`, so the composed dump body fits the
|
|
982
|
+
// inline byte cap by construction.
|
|
983
|
+
|
|
972
984
|
#willOverflow(dataBytes: number): boolean {
|
|
973
985
|
// Triggers file mirroring as soon as the next chunk would push us over
|
|
974
|
-
// the tail budget
|
|
975
|
-
return this.#bufferBytes + dataBytes > this.#spillThreshold;
|
|
986
|
+
// the tail budget — i.e. the first byte that could be lost from memory.
|
|
987
|
+
return this.#bufferBytes + dataBytes > this.#spillThreshold - this.#headBytes;
|
|
976
988
|
}
|
|
977
989
|
|
|
978
990
|
#pushTail(chunk: string, dataBytes: number): void {
|
|
979
991
|
if (dataBytes === 0) return;
|
|
980
992
|
|
|
981
|
-
const threshold = this.#spillThreshold;
|
|
993
|
+
const threshold = Math.max(0, this.#spillThreshold - this.#headBytes);
|
|
982
994
|
const willOverflow = this.#bufferBytes + dataBytes > threshold;
|
|
983
995
|
|
|
984
996
|
if (!willOverflow) {
|
|
@@ -118,7 +118,7 @@ export interface TurnRecoveryHost {
|
|
|
118
118
|
waitForSessionMessagePersistence(message: AssistantMessage): Promise<void>;
|
|
119
119
|
appendSessionMessage(message: AssistantMessage): void;
|
|
120
120
|
sessionMessageAlreadyPersisted(message: AssistantMessage): boolean;
|
|
121
|
-
setModelWithProviderSessionReset(model: Model): void
|
|
121
|
+
setModelWithProviderSessionReset(model: Model): Promise<void>;
|
|
122
122
|
resetCurrentResponsesProviderSession(reason: string): void;
|
|
123
123
|
maybeAutoRedeemCodexReset(): Promise<boolean>;
|
|
124
124
|
runAutoCompaction(
|
|
@@ -1028,7 +1028,7 @@ export class TurnRecovery {
|
|
|
1028
1028
|
? requestedThinkingLevel
|
|
1029
1029
|
: clampThinkingLevelToCeiling(candidate, requestedThinkingLevel, this.#host.thinkingLevelCeiling());
|
|
1030
1030
|
const candidateSelector = formatModelStringWithRouting(candidate);
|
|
1031
|
-
this.#host.setModelWithProviderSessionReset(candidate);
|
|
1031
|
+
await this.#host.setModelWithProviderSessionReset(candidate);
|
|
1032
1032
|
this.#host.sessionManager.appendModelChange(candidateSelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
1033
1033
|
this.#host.settings.getStorage()?.recordModelUsage(candidateSelector);
|
|
1034
1034
|
this.#host.setThinkingLevel(nextThinkingLevel);
|
|
@@ -1147,7 +1147,7 @@ export class TurnRecovery {
|
|
|
1147
1147
|
const apiKey = await this.#host.modelRegistry.getApiKey(baseModel, this.#host.sessionId());
|
|
1148
1148
|
if (!apiKey) return false;
|
|
1149
1149
|
const baseSelector = formatModelStringWithRouting(baseModel);
|
|
1150
|
-
this.#host.setModelWithProviderSessionReset(baseModel);
|
|
1150
|
+
await this.#host.setModelWithProviderSessionReset(baseModel);
|
|
1151
1151
|
this.#host.sessionManager.appendModelChange(baseSelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
1152
1152
|
this.#host.settings.getStorage()?.recordModelUsage(baseSelector);
|
|
1153
1153
|
await this.#host.emitSessionEvent({
|
|
@@ -1201,7 +1201,7 @@ export class TurnRecovery {
|
|
|
1201
1201
|
const thinkingToApply =
|
|
1202
1202
|
currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
|
|
1203
1203
|
const primarySelector = formatModelStringWithRouting(primaryModel);
|
|
1204
|
-
this.#host.setModelWithProviderSessionReset(primaryModel);
|
|
1204
|
+
await this.#host.setModelWithProviderSessionReset(primaryModel);
|
|
1205
1205
|
this.#host.sessionManager.appendModelChange(primarySelector, EPHEMERAL_MODEL_CHANGE_ROLE);
|
|
1206
1206
|
this.#host.settings.getStorage()?.recordModelUsage(primarySelector);
|
|
1207
1207
|
this.#host.setThinkingLevel(thinkingToApply);
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
renderChangelogEntries,
|
|
54
54
|
} from "../utils/changelog";
|
|
55
55
|
import { copyToClipboard } from "../utils/clipboard";
|
|
56
|
+
import type { InspectImageMode } from "../utils/inspect-image-mode";
|
|
56
57
|
import { CollabQrCodeComponent } from "./helpers/collab-qrcode";
|
|
57
58
|
import { buildContextReportText } from "./helpers/context-report";
|
|
58
59
|
import { formatDuration } from "./helpers/format";
|
|
@@ -151,6 +152,33 @@ async function applyComputerUseToggle(session: AgentSession, enable: boolean): P
|
|
|
151
152
|
: "Computer use disabled for this session.";
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
/** Session-effective `/vision status` line. */
|
|
156
|
+
function formatVisionStatus(session: AgentSession): string {
|
|
157
|
+
const { mode, active, model } = session.inspectImageState();
|
|
158
|
+
const override = session.getInspectImageModeOverride();
|
|
159
|
+
const modelObj = session.model;
|
|
160
|
+
const capability = modelObj
|
|
161
|
+
? modelObj.input.includes("image")
|
|
162
|
+
? "native image input"
|
|
163
|
+
: "no native image input"
|
|
164
|
+
: "no active model";
|
|
165
|
+
return [
|
|
166
|
+
`inspect_image: ${active ? "active" : "inactive"}`,
|
|
167
|
+
`mode: ${mode}${override ? " (session override)" : ""}`,
|
|
168
|
+
...(override ? [`configured: ${session.settings.get("inspect_image.mode")}`] : []),
|
|
169
|
+
`model: ${model ?? "none"} (${capability})`,
|
|
170
|
+
].join(" · ");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Applies a `/vision` mode for this session and returns the operator feedback line. */
|
|
174
|
+
async function applyVisionMode(session: AgentSession, mode: InspectImageMode): Promise<string> {
|
|
175
|
+
const applied = await session.setInspectImageMode(mode);
|
|
176
|
+
if (!applied) {
|
|
177
|
+
return "inspect_image is unavailable in this session.";
|
|
178
|
+
}
|
|
179
|
+
return `Vision mode: ${mode}. ${formatVisionStatus(session)}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
154
182
|
const AUTOCOMPLETE_DETAIL_LIMIT = 48;
|
|
155
183
|
|
|
156
184
|
function shortDetail(value: string, limit = AUTOCOMPLETE_DETAIL_LIMIT): string {
|
|
@@ -440,12 +468,15 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
440
468
|
},
|
|
441
469
|
{
|
|
442
470
|
name: "guided-goal",
|
|
443
|
-
description: "
|
|
471
|
+
description: "Have the agent interview you in chat, then set up goal mode",
|
|
444
472
|
inlineHint: "[rough objective]",
|
|
445
473
|
allowArgs: true,
|
|
446
474
|
handleTui: async (command, runtime) => {
|
|
447
|
-
await
|
|
475
|
+
// Clear the slash draft BEFORE the await: the handler blocks for the
|
|
476
|
+
// whole kickoff turn, and a post-await clear would wipe an answer the
|
|
477
|
+
// user starts typing while the first interview question streams.
|
|
448
478
|
runtime.ctx.editor.setText("");
|
|
479
|
+
await runtime.ctx.handleGuidedGoalCommand(command.args || undefined);
|
|
449
480
|
},
|
|
450
481
|
},
|
|
451
482
|
{
|
|
@@ -647,6 +678,47 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
647
678
|
runtime.ctx.editor.setText("");
|
|
648
679
|
},
|
|
649
680
|
},
|
|
681
|
+
{
|
|
682
|
+
name: "vision",
|
|
683
|
+
description: "Control the inspect_image vision-delegation tool for this session",
|
|
684
|
+
acpDescription: "Toggle vision delegation",
|
|
685
|
+
acpInputHint: "[on|off|auto|status]",
|
|
686
|
+
subcommands: [
|
|
687
|
+
{ name: "on", description: "Always expose inspect_image this session" },
|
|
688
|
+
{ name: "off", description: "Never expose inspect_image this session" },
|
|
689
|
+
{ name: "auto", description: "Follow inspect_image.mode (auto hides it for vision-capable models)" },
|
|
690
|
+
{ name: "status", description: "Show inspect_image status" },
|
|
691
|
+
],
|
|
692
|
+
allowArgs: true,
|
|
693
|
+
getTuiAutocompleteDescription: runtime => `Vision: ${runtime.ctx.session.inspectImageState().mode}`,
|
|
694
|
+
handle: async (command, runtime) => {
|
|
695
|
+
const arg = command.args.trim().toLowerCase();
|
|
696
|
+
if (arg === "status") {
|
|
697
|
+
await runtime.output(formatVisionStatus(runtime.session));
|
|
698
|
+
return commandConsumed();
|
|
699
|
+
}
|
|
700
|
+
if (arg === "on" || arg === "off" || arg === "auto") {
|
|
701
|
+
await runtime.output(await applyVisionMode(runtime.session, arg));
|
|
702
|
+
return commandConsumed();
|
|
703
|
+
}
|
|
704
|
+
return usage("Usage: /vision [on|off|auto|status]", runtime);
|
|
705
|
+
},
|
|
706
|
+
handleTui: async (command, runtime) => {
|
|
707
|
+
const arg = command.args.trim().toLowerCase();
|
|
708
|
+
if (arg === "status") {
|
|
709
|
+
runtime.ctx.showStatus(formatVisionStatus(runtime.ctx.session));
|
|
710
|
+
runtime.ctx.editor.setText("");
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (arg === "on" || arg === "off" || arg === "auto") {
|
|
714
|
+
runtime.ctx.showStatus(await applyVisionMode(runtime.ctx.session, arg));
|
|
715
|
+
runtime.ctx.editor.setText("");
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
runtime.ctx.showStatus("Usage: /vision [on|off|auto|status]");
|
|
719
|
+
runtime.ctx.editor.setText("");
|
|
720
|
+
},
|
|
721
|
+
},
|
|
650
722
|
{
|
|
651
723
|
name: "prewalk",
|
|
652
724
|
description: "Switch to a fast/cheap model at the next action (works even without --prewalk)",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership marker for task-isolation sandboxes under `~/.omp/wt/`.
|
|
3
|
+
*
|
|
4
|
+
* Each isolation base dir (`ensureIsolation` in {@link ./worktree}) holds a
|
|
5
|
+
* compact `m` mount plus this marker file naming the omp process that created
|
|
6
|
+
* it. `omp worktree clear` consults the marker so it can distinguish a live
|
|
7
|
+
* subagent's sandbox from a crashed run's leftover instead of deleting both.
|
|
8
|
+
*/
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { $ } from "bun";
|
|
11
|
+
|
|
12
|
+
/** Marker file written into a task-isolation base dir identifying its owner. */
|
|
13
|
+
export const ISOLATION_OWNER_FILE = ".omp-isolation-owner.json";
|
|
14
|
+
|
|
15
|
+
/** Recorded owner of a task-isolation sandbox. */
|
|
16
|
+
export interface IsolationOwner {
|
|
17
|
+
/** PID of the omp process that created and owns the sandbox. */
|
|
18
|
+
pid: number;
|
|
19
|
+
/** Task id the sandbox was materialised for. */
|
|
20
|
+
id: string;
|
|
21
|
+
/**
|
|
22
|
+
* Process-instance start-time token for {@link pid}, when the OS can report
|
|
23
|
+
* it. Distinguishes the owning process from an unrelated process that later
|
|
24
|
+
* inherits a recycled pid, so a crashed sandbox is never pinned live.
|
|
25
|
+
*/
|
|
26
|
+
startToken?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Boot-stable start-time token for `pid`, or `null` when the process is gone or
|
|
31
|
+
* the platform cannot report it. Read from the same source on write and
|
|
32
|
+
* validate so an exact string compare rejects a recycled pid.
|
|
33
|
+
*
|
|
34
|
+
* Linux reads `/proc/<pid>/stat` field 22 (start time in clock ticks since
|
|
35
|
+
* boot); other Unixes shell out to `ps -o lstart`. Platforms that report
|
|
36
|
+
* neither (e.g. Windows) yield `null`, degrading to a pid-only liveness check.
|
|
37
|
+
*/
|
|
38
|
+
async function processStartToken(pid: number): Promise<string | null> {
|
|
39
|
+
if (process.platform === "linux") {
|
|
40
|
+
let stat: string;
|
|
41
|
+
try {
|
|
42
|
+
stat = await Bun.file(`/proc/${pid}/stat`).text();
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
// The comm field (2) may embed spaces and parens, so parse the numeric
|
|
47
|
+
// fields after the final ')'. `starttime` is field 22 overall, i.e. the
|
|
48
|
+
// 20th token once `pid` and `(comm)` are dropped.
|
|
49
|
+
const commEnd = stat.lastIndexOf(")");
|
|
50
|
+
if (commEnd < 0) return null;
|
|
51
|
+
const starttime = stat.slice(commEnd + 2).split(" ")[19];
|
|
52
|
+
return starttime && starttime.length > 0 ? starttime : null;
|
|
53
|
+
}
|
|
54
|
+
const res = await $`ps -o lstart= -p ${pid}`.quiet().nothrow();
|
|
55
|
+
if (res.exitCode !== 0) return null;
|
|
56
|
+
const started = res.text().trim();
|
|
57
|
+
return started.length > 0 ? started : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record the current process as owner of the sandbox rooted at `baseDir`.
|
|
62
|
+
*
|
|
63
|
+
* Written before the isolation backend materialises `m` so a concurrent
|
|
64
|
+
* `omp worktree clear` never sees an owner-less sandbox mid-creation.
|
|
65
|
+
*/
|
|
66
|
+
export async function writeIsolationOwner(baseDir: string, id: string): Promise<void> {
|
|
67
|
+
const startToken = await processStartToken(process.pid);
|
|
68
|
+
const owner: IsolationOwner = { pid: process.pid, id, ...(startToken ? { startToken } : {}) };
|
|
69
|
+
await Bun.write(path.join(baseDir, ISOLATION_OWNER_FILE), JSON.stringify(owner));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether a live omp process still owns the sandbox at `baseDir`.
|
|
74
|
+
*
|
|
75
|
+
* A missing or malformed marker means no verifiable owner — a crashed run or a
|
|
76
|
+
* sandbox from before markers existed, both safe to reclaim. `process.kill(pid,
|
|
77
|
+
* 0)` can fail with `EPERM` even when the process is alive, so only an explicit
|
|
78
|
+
* `ESRCH` ("no such process") counts as dead; any other error is treated as
|
|
79
|
+
* alive to avoid deleting a sandbox that is actually in use. When the marker
|
|
80
|
+
* carries a {@link IsolationOwner.startToken}, a live pid whose current token no
|
|
81
|
+
* longer matches is a recycled pid — a different process — and counts as dead.
|
|
82
|
+
*/
|
|
83
|
+
export async function hasLiveIsolationOwner(baseDir: string): Promise<boolean> {
|
|
84
|
+
let decoded: unknown;
|
|
85
|
+
try {
|
|
86
|
+
decoded = await Bun.file(path.join(baseDir, ISOLATION_OWNER_FILE)).json();
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (typeof decoded !== "object" || decoded === null || !("pid" in decoded)) return false;
|
|
91
|
+
const pid = decoded.pid;
|
|
92
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
93
|
+
try {
|
|
94
|
+
process.kill(pid, 0);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if ((err as NodeJS.ErrnoException).code === "ESRCH") return false;
|
|
97
|
+
}
|
|
98
|
+
// The pid is live (or unknowable via EPERM). Reject a recycled pid: if the
|
|
99
|
+
// marker pinned the owner's start-time token, the process wearing that pid
|
|
100
|
+
// now must still present the same token.
|
|
101
|
+
if ("startToken" in decoded && typeof decoded.startToken === "string" && decoded.startToken.length > 0) {
|
|
102
|
+
const current = await processStartToken(pid);
|
|
103
|
+
if (current !== null && current !== decoded.startToken) return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
package/src/task/worktree.ts
CHANGED
|
@@ -6,6 +6,7 @@ import * as natives from "@oh-my-pi/pi-natives";
|
|
|
6
6
|
import { getWorktreeDir, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import * as git from "../utils/git";
|
|
8
8
|
import * as jj from "../utils/jj";
|
|
9
|
+
import { writeIsolationOwner } from "./isolation-ownership";
|
|
9
10
|
import { mapWithConcurrencyLimit } from "./parallel";
|
|
10
11
|
|
|
11
12
|
const { IsoBackendKind } = natives;
|
|
@@ -434,6 +435,13 @@ export async function ensureIsolation(
|
|
|
434
435
|
|
|
435
436
|
for (const candidate of candidates) {
|
|
436
437
|
await fs.rm(baseDir, { recursive: true, force: true });
|
|
438
|
+
// Claim ownership before the backend materialises `m`. Backends only
|
|
439
|
+
// create/replace `mergedDir` (and overlay upper/work), never the base
|
|
440
|
+
// dir, so the marker survives `isoStart` — and a concurrent
|
|
441
|
+
// `omp worktree clear` never sees this sandbox without a live owner,
|
|
442
|
+
// even while a large clone is still in progress.
|
|
443
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
444
|
+
await writeIsolationOwner(baseDir, id);
|
|
437
445
|
try {
|
|
438
446
|
await natives.isoStart(candidate, repoRoot, mergedDir);
|
|
439
447
|
// Sever the isolation's git metadata from the source checkout. Copy
|
package/src/tools/ask.ts
CHANGED
|
@@ -36,7 +36,7 @@ import type { ExtensionUISelectItem } from "../extensibility/extensions";
|
|
|
36
36
|
import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
|
|
37
37
|
import askDescription from "../prompts/tools/ask.md" with { type: "text" };
|
|
38
38
|
import { vocalizer } from "../tts/vocalizer";
|
|
39
|
-
import { framedBlock, renderStatusLine } from "../tui";
|
|
39
|
+
import { framedBlock, outputBlockContentWidth, renderStatusLine } from "../tui";
|
|
40
40
|
import type { ToolSession } from ".";
|
|
41
41
|
import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
|
|
42
42
|
import { ToolAbortError } from "./tool-errors";
|
|
@@ -1276,7 +1276,7 @@ export const askToolRenderer = {
|
|
|
1276
1276
|
const mdTheme = getMarkdownTheme();
|
|
1277
1277
|
const accentStyle = { color: (t: string) => uiTheme.fg("accent", t) };
|
|
1278
1278
|
const md = (text: string, width: number) =>
|
|
1279
|
-
new Markdown(text, 1, 0, mdTheme, accentStyle).render(Math.max(1, width
|
|
1279
|
+
new Markdown(text, 1, 0, mdTheme, accentStyle).render(Math.max(1, outputBlockContentWidth(width) + 1));
|
|
1280
1280
|
|
|
1281
1281
|
// Multi-part questions: one divider-labelled section per question.
|
|
1282
1282
|
// Call args are untrusted (partially streamed or model-mangled) and a
|
|
@@ -1345,7 +1345,7 @@ export const askToolRenderer = {
|
|
|
1345
1345
|
const mdTheme = getMarkdownTheme();
|
|
1346
1346
|
const accentStyle = { color: (t: string) => uiTheme.fg("accent", t) };
|
|
1347
1347
|
const md = (text: string, width: number) =>
|
|
1348
|
-
new Markdown(text, 1, 0, mdTheme, accentStyle).render(Math.max(1, width
|
|
1348
|
+
new Markdown(text, 1, 0, mdTheme, accentStyle).render(Math.max(1, outputBlockContentWidth(width) + 1));
|
|
1349
1349
|
|
|
1350
1350
|
if (!details) {
|
|
1351
1351
|
const txt = result.content[0];
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -12,7 +12,14 @@ import { normalizeToLF } from "../edit/normalize";
|
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
13
|
import type { Theme } from "../modes/theme/theme";
|
|
14
14
|
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
Ellipsis,
|
|
17
|
+
fileHyperlink,
|
|
18
|
+
framedBlock,
|
|
19
|
+
outputBlockContentWidth,
|
|
20
|
+
renderStatusLine,
|
|
21
|
+
truncateToWidth,
|
|
22
|
+
} from "../tui";
|
|
16
23
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
17
24
|
import type { ToolSession } from ".";
|
|
18
25
|
import { truncateForPrompt } from "./approval";
|
|
@@ -695,7 +702,7 @@ export const astEditToolRenderer = {
|
|
|
695
702
|
}
|
|
696
703
|
return framedBlock(uiTheme, width => {
|
|
697
704
|
const changeLines = buildChangeBody(changeGroups, Boolean(options.expanded), COLLAPSED_CHANGE_LIMIT, uiTheme);
|
|
698
|
-
const innerWidth =
|
|
705
|
+
const innerWidth = outputBlockContentWidth(width);
|
|
699
706
|
const bodyLines = [...changeLines, ...extraLines].map(l => truncateToWidth(l, innerWidth, Ellipsis.Omit));
|
|
700
707
|
while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
|
|
701
708
|
return {
|
package/src/tools/bash.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { invalidateGithubCacheForBashCommand } from "./gh-cache-invalidation";
|
|
|
37
37
|
import {
|
|
38
38
|
formatStyledTruncationWarning,
|
|
39
39
|
type OutputMeta,
|
|
40
|
+
resolveInlineByteCapBudget,
|
|
40
41
|
stripOutputNotice,
|
|
41
42
|
stripRawOutputArtifactNotice,
|
|
42
43
|
} from "./output-meta";
|
|
@@ -655,6 +656,18 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
655
656
|
details.exitCode = exitCode;
|
|
656
657
|
}
|
|
657
658
|
|
|
659
|
+
// Final-defense inline cap config, shared by the timeout and normal
|
|
660
|
+
// completion paths. The sink already bounds inline bodies to the spill
|
|
661
|
+
// threshold, so with the notice slack this only fires on paths that
|
|
662
|
+
// bypass the sink (client-bridge terminals, minimizer misses). When the
|
|
663
|
+
// sink spilled, its artifact already holds the full raw stream — reuse
|
|
664
|
+
// that id instead of saving a second (already-truncated) copy, so the
|
|
665
|
+
// `[raw output: artifact://N]` footer and the truncation notice agree.
|
|
666
|
+
const inlineCap = {
|
|
667
|
+
maxBytes: resolveInlineByteCapBudget(this.session.settings),
|
|
668
|
+
saveArtifact: (full: string) => result.artifactId ?? saveBashOriginalArtifact(this.session, full),
|
|
669
|
+
};
|
|
670
|
+
|
|
658
671
|
if (isTimeout) {
|
|
659
672
|
details.timedOut = true;
|
|
660
673
|
const message =
|
|
@@ -664,9 +677,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
664
677
|
if (!normalizeResultOutput(result).startsWith(`[${message}]\n`)) {
|
|
665
678
|
outputLines.push("", `[${message}]`);
|
|
666
679
|
}
|
|
667
|
-
const timeoutOutputText = await enforceInlineByteCap(outputLines.join("\n"),
|
|
668
|
-
saveArtifact: full => saveBashOriginalArtifact(this.session, full),
|
|
669
|
-
});
|
|
680
|
+
const timeoutOutputText = await enforceInlineByteCap(outputLines.join("\n"), inlineCap);
|
|
670
681
|
return toolResult(details)
|
|
671
682
|
.text(timeoutOutputText)
|
|
672
683
|
.truncationFromSummary(result, { direction: "tail" })
|
|
@@ -677,12 +688,8 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
677
688
|
// Non-timeout cancellations and missing exit status still propagate as thrown errors.
|
|
678
689
|
this.#throwIfUnfinished(result, timeoutSec, outputText);
|
|
679
690
|
|
|
680
|
-
//
|
|
681
|
-
|
|
682
|
-
// ~DEFAULT_MAX_BYTES inline. No-op for already-bounded output.
|
|
683
|
-
const cappedOutputText = await enforceInlineByteCap(outputText, {
|
|
684
|
-
saveArtifact: full => saveBashOriginalArtifact(this.session, full),
|
|
685
|
-
});
|
|
691
|
+
// No-op for already-bounded output; see `inlineCap` above.
|
|
692
|
+
const cappedOutputText = await enforceInlineByteCap(outputText, inlineCap);
|
|
686
693
|
|
|
687
694
|
const resultBuilder = toolResult(details)
|
|
688
695
|
.text(cappedOutputText)
|
|
@@ -37,7 +37,6 @@ import type { CmuxSocketClient } from "./socket-client";
|
|
|
37
37
|
interface ScreenshotOptions {
|
|
38
38
|
selector?: string;
|
|
39
39
|
fullPage?: boolean;
|
|
40
|
-
save?: string;
|
|
41
40
|
silent?: boolean;
|
|
42
41
|
encoding?: "base64" | "binary";
|
|
43
42
|
}
|
|
@@ -487,7 +486,7 @@ export class CmuxTab {
|
|
|
487
486
|
return content;
|
|
488
487
|
}
|
|
489
488
|
|
|
490
|
-
async screenshot(opts: ScreenshotOptions = {}): Promise<
|
|
489
|
+
async screenshot(opts: ScreenshotOptions = {}): Promise<string> {
|
|
491
490
|
const context = this.#requireRunContext("tab.screenshot()");
|
|
492
491
|
// The cmux daemon's `browser.screenshot` captures the surface viewport
|
|
493
492
|
// only — it has no element-clip or full-page mode, and Bun.Image cannot
|
|
@@ -518,20 +517,16 @@ export class CmuxTab {
|
|
|
518
517
|
excludeWebP: context.session.excludeWebP,
|
|
519
518
|
},
|
|
520
519
|
);
|
|
521
|
-
const
|
|
522
|
-
const returnedPath = typeof result.path === "string" && result.path.length > 0 ? result.path : undefined;
|
|
523
|
-
const saveFullRes = !!(explicitPath || context.session.browserScreenshotDir || returnedPath);
|
|
520
|
+
const saveFullRes = !!context.session.browserScreenshotDir;
|
|
524
521
|
const savedBuffer = saveFullRes ? buffer : Buffer.from(resized.buffer);
|
|
525
522
|
const savedMimeType = saveFullRes ? captureMime : resized.mimeType;
|
|
526
523
|
const ext = savedMimeType === "image/webp" ? "webp" : savedMimeType === "image/jpeg" ? "jpg" : "png";
|
|
527
|
-
const dest =
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
)
|
|
534
|
-
: (returnedPath ?? path.join(os.tmpdir(), `omp-sshots-${Snowflake.next()}.${ext}`)));
|
|
524
|
+
const dest = context.session.browserScreenshotDir
|
|
525
|
+
? path.join(
|
|
526
|
+
context.session.browserScreenshotDir,
|
|
527
|
+
`screenshot-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, -1)}.${ext}`,
|
|
528
|
+
)
|
|
529
|
+
: path.join(os.tmpdir(), `omp-sshots-${Snowflake.next()}.${ext}`);
|
|
535
530
|
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
536
531
|
await Bun.write(dest, savedBuffer);
|
|
537
532
|
const info: ScreenshotResult = {
|
|
@@ -556,7 +551,7 @@ export class CmuxTab {
|
|
|
556
551
|
context.output.push({ type: "text", text: lines.join("\n") });
|
|
557
552
|
context.output.push({ type: "image", data: resized.data, mimeType: resized.mimeType });
|
|
558
553
|
}
|
|
559
|
-
return
|
|
554
|
+
return dest;
|
|
560
555
|
}
|
|
561
556
|
|
|
562
557
|
async waitForUrl(pattern: string | RegExp, opts?: { timeout?: number }): Promise<string> {
|
|
@@ -11,7 +11,6 @@ import type {
|
|
|
11
11
|
ElementHandle,
|
|
12
12
|
ElementScreenshotOptions,
|
|
13
13
|
HTTPResponse,
|
|
14
|
-
ImageFormat,
|
|
15
14
|
KeyInput,
|
|
16
15
|
Page,
|
|
17
16
|
SerializedAXNode,
|
|
@@ -217,7 +216,6 @@ export function resolveWaitTimeout(cellTimeoutMs: number, explicit?: number): nu
|
|
|
217
216
|
interface ScreenshotOptions {
|
|
218
217
|
selector?: string;
|
|
219
218
|
fullPage?: boolean;
|
|
220
|
-
save?: string;
|
|
221
219
|
silent?: boolean;
|
|
222
220
|
}
|
|
223
221
|
|
|
@@ -233,7 +231,7 @@ interface TabApi {
|
|
|
233
231
|
): Promise<void>;
|
|
234
232
|
observe(opts?: { includeAll?: boolean; viewportOnly?: boolean }): Promise<Observation>;
|
|
235
233
|
ariaSnapshot(selector?: string, opts?: AriaSnapshotOptions): Promise<string>;
|
|
236
|
-
screenshot(opts?: ScreenshotOptions): Promise<
|
|
234
|
+
screenshot(opts?: ScreenshotOptions): Promise<string>;
|
|
237
235
|
extract(format?: ReadableFormat): Promise<string>;
|
|
238
236
|
click(selector: string): Promise<void>;
|
|
239
237
|
type(selector: string, text: string): Promise<void>;
|
|
@@ -714,19 +712,6 @@ export function describeScreenshot(opts?: ScreenshotOptions): string {
|
|
|
714
712
|
return "tab.screenshot()";
|
|
715
713
|
}
|
|
716
714
|
|
|
717
|
-
/** Map an explicit save path's extension to a puppeteer capture format (default png). */
|
|
718
|
-
export function imageFormatForPath(filePath: string): ImageFormat {
|
|
719
|
-
switch (path.extname(filePath).toLowerCase()) {
|
|
720
|
-
case ".webp":
|
|
721
|
-
return "webp";
|
|
722
|
-
case ".jpg":
|
|
723
|
-
case ".jpeg":
|
|
724
|
-
return "jpeg";
|
|
725
|
-
default:
|
|
726
|
-
return "png";
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
715
|
/** Summarize still-running helpers (oldest first) so a cell timeout names what stalled. */
|
|
731
716
|
export function describeInflight(inflight: Map<number, InflightOp>): string {
|
|
732
717
|
const now = Date.now();
|
|
@@ -1532,7 +1517,7 @@ export class WorkerCore {
|
|
|
1532
1517
|
screenshots: ScreenshotResult[],
|
|
1533
1518
|
signal: AbortSignal | undefined,
|
|
1534
1519
|
opts: ScreenshotOptions = {},
|
|
1535
|
-
): Promise<
|
|
1520
|
+
): Promise<string> {
|
|
1536
1521
|
const page = this.#requirePage();
|
|
1537
1522
|
// Multiple tabs can share one Chromium (sibling headless tabs on a shared
|
|
1538
1523
|
// endpoint, cdp/app attach). CDP `Page.captureScreenshot` reads the
|
|
@@ -1542,12 +1527,8 @@ export class WorkerCore {
|
|
|
1542
1527
|
// already-active or freshly-closed target never fails the capture.
|
|
1543
1528
|
await untilAborted(signal, () => page.bringToFront()).catch(() => undefined);
|
|
1544
1529
|
const fullPage = opts.selector ? false : (opts.fullPage ?? false);
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
// of PNG bytes hiding behind a .webp name. Unknown/missing extensions stay PNG.
|
|
1548
|
-
const explicitPath = opts.save ? resolveToCwd(opts.save, session.cwd) : undefined;
|
|
1549
|
-
const captureType = explicitPath ? imageFormatForPath(explicitPath) : "png";
|
|
1550
|
-
const captureMime = `image/${captureType}` as const;
|
|
1530
|
+
const captureType = "png";
|
|
1531
|
+
const captureMime = "image/png" as const;
|
|
1551
1532
|
let buffer: Buffer;
|
|
1552
1533
|
if (opts.selector) {
|
|
1553
1534
|
const handle =
|
|
@@ -1581,20 +1562,16 @@ export class WorkerCore {
|
|
|
1581
1562
|
{ type: "image", data: buffer.toBase64(), mimeType: captureMime },
|
|
1582
1563
|
{ maxWidth: 1024, maxHeight: 1024, maxBytes: 150 * 1024, jpegQuality: 70, excludeWebP: session.excludeWebP },
|
|
1583
1564
|
);
|
|
1584
|
-
const saveFullRes = !!
|
|
1565
|
+
const saveFullRes = !!session.browserScreenshotDir;
|
|
1585
1566
|
const savedBuffer = saveFullRes ? buffer : resized.buffer;
|
|
1586
1567
|
const savedMimeType = saveFullRes ? captureMime : resized.mimeType;
|
|
1587
|
-
// Names must match the bytes we actually write: full-res follows the capture
|
|
1588
|
-
// format, the resized buffer is whichever of PNG/JPEG/WebP encoded smallest.
|
|
1589
1568
|
const ext = savedMimeType === "image/webp" ? "webp" : savedMimeType === "image/jpeg" ? "jpg" : "png";
|
|
1590
|
-
const dest =
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
)
|
|
1597
|
-
: path.join(os.tmpdir(), `omp-sshots-${Snowflake.next()}.${ext}`));
|
|
1569
|
+
const dest = session.browserScreenshotDir
|
|
1570
|
+
? path.join(
|
|
1571
|
+
session.browserScreenshotDir,
|
|
1572
|
+
`screenshot-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, -1)}.${ext}`,
|
|
1573
|
+
)
|
|
1574
|
+
: path.join(os.tmpdir(), `omp-sshots-${Snowflake.next()}.${ext}`);
|
|
1598
1575
|
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
1599
1576
|
await Bun.write(dest, savedBuffer);
|
|
1600
1577
|
const info: ScreenshotResult = {
|
|
@@ -1616,7 +1593,7 @@ export class WorkerCore {
|
|
|
1616
1593
|
output.push({ type: "text", text: lines.join("\n") });
|
|
1617
1594
|
output.push({ type: "image", data: resized.data, mimeType: resized.mimeType });
|
|
1618
1595
|
}
|
|
1619
|
-
return
|
|
1596
|
+
return dest;
|
|
1620
1597
|
}
|
|
1621
1598
|
|
|
1622
1599
|
async #drag(from: DragTarget, to: DragTarget, signal: AbortSignal): Promise<void> {
|
package/src/tools/browser.ts
CHANGED
|
@@ -163,11 +163,11 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
163
163
|
},
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
|
-
caption: "
|
|
166
|
+
caption: "Capture a screenshot and return its saved path",
|
|
167
167
|
call: {
|
|
168
168
|
action: "run",
|
|
169
169
|
name: "docs",
|
|
170
|
-
code: "await tab.screenshot();",
|
|
170
|
+
code: "return await tab.screenshot();",
|
|
171
171
|
},
|
|
172
172
|
},
|
|
173
173
|
{
|
package/src/tools/gh-renderer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Component, padding, Text, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
3
3
|
import type { Theme, ThemeColor } from "../modes/theme/theme";
|
|
4
|
-
import { framedBlock, renderStatusLine } from "../tui";
|
|
4
|
+
import { framedBlock, outputBlockContentWidth, renderStatusLine } from "../tui";
|
|
5
5
|
import type {
|
|
6
6
|
GhRunWatchFailedLogDetails,
|
|
7
7
|
GhRunWatchJobDetails,
|
|
@@ -364,7 +364,7 @@ function renderFallbackComponent(
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
return framedBlock(theme, width => {
|
|
367
|
-
const lineWidth =
|
|
367
|
+
const lineWidth = outputBlockContentWidth(width || FALLBACK_WIDTH);
|
|
368
368
|
const expanded = options.expanded;
|
|
369
369
|
const limit = expanded ? allLines.length : Math.min(allLines.length, PREVIEW_LIMITS.OUTPUT_EXPANDED);
|
|
370
370
|
const visible = allLines.slice(0, limit);
|
|
@@ -464,7 +464,7 @@ export const githubToolRenderer = {
|
|
|
464
464
|
uiTheme,
|
|
465
465
|
);
|
|
466
466
|
return framedBlock(uiTheme, width => {
|
|
467
|
-
const innerWidth =
|
|
467
|
+
const innerWidth = outputBlockContentWidth(width || FALLBACK_WIDTH);
|
|
468
468
|
const sections = buildWatchSections(watch, uiTheme, options, innerWidth);
|
|
469
469
|
return {
|
|
470
470
|
header,
|