@oh-my-pi/pi-coding-agent 16.2.12 → 16.3.0
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 +115 -7
- package/dist/cli.js +5996 -5945
- package/dist/types/advisor/config.d.ts +4 -2
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +9 -3
- package/dist/types/commit/model-selection.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +12 -10
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/models-config.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +28 -5
- package/dist/types/edit/modes/patch.d.ts +11 -0
- package/dist/types/eval/agent-bridge.d.ts +5 -1
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +86 -2
- package/dist/types/extensibility/skills.d.ts +2 -1
- package/dist/types/mnemopi/state.d.ts +12 -0
- package/dist/types/modes/components/agent-hub.d.ts +9 -5
- package/dist/types/modes/components/status-line/component.test.d.ts +1 -0
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +6 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +17 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +30 -3
- package/dist/types/modes/interactive-mode.d.ts +12 -7
- package/dist/types/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +64 -1
- package/dist/types/modes/session-teardown.d.ts +63 -0
- package/dist/types/modes/session-teardown.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +9 -4
- package/dist/types/modes/types.d.ts +2 -3
- package/dist/types/modes/utils/copy-targets.d.ts +2 -0
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +1 -0
- package/dist/types/sdk.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +46 -13
- package/dist/types/session/exit-diagnostics.d.ts +48 -0
- package/dist/types/session/session-loader.d.ts +5 -0
- package/dist/types/task/executor.d.ts +12 -5
- package/dist/types/task/parallel.d.ts +1 -0
- package/dist/types/task/render.test.d.ts +1 -0
- package/dist/types/thinking.d.ts +2 -0
- package/dist/types/tools/checkpoint.d.ts +8 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +11 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/output-meta.d.ts +7 -0
- package/dist/types/tools/output-schema-validator.d.ts +7 -4
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/render-utils.d.ts +4 -1
- package/dist/types/utils/git.d.ts +47 -2
- package/dist/types/web/search/provider.d.ts +7 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/config.ts +4 -2
- package/src/cli/bench-cli.ts +7 -2
- package/src/collab/guest.ts +94 -5
- package/src/collab/host.ts +80 -3
- package/src/collab/protocol.ts +9 -2
- package/src/commit/model-selection.ts +8 -2
- package/src/config/keybindings.ts +3 -1
- package/src/config/model-discovery.ts +66 -2
- package/src/config/model-registry.ts +7 -3
- package/src/config/model-resolver.ts +51 -23
- package/src/config/models-config-schema.ts +13 -0
- package/src/config/settings-schema.ts +44 -14
- package/src/edit/hashline/diff.ts +13 -2
- package/src/edit/index.ts +58 -8
- package/src/edit/modes/patch.ts +53 -18
- package/src/edit/streaming.ts +7 -6
- package/src/eval/__tests__/agent-bridge.test.ts +57 -1
- package/src/eval/agent-bridge.ts +15 -5
- package/src/exec/bash-executor.ts +7 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +534 -16
- package/src/extensibility/skills.ts +29 -6
- package/src/goals/guided-setup.ts +4 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +11 -14
- package/src/main.ts +38 -10
- package/src/mnemopi/state.ts +43 -7
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/agent-hub.ts +10 -2
- package/src/modes/components/agent-transcript-viewer.ts +39 -7
- package/src/modes/components/assistant-message.ts +16 -10
- package/src/modes/components/chat-transcript-builder.ts +11 -1
- package/src/modes/components/custom-editor.test.ts +20 -0
- package/src/modes/components/hook-selector.ts +44 -25
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/status-line/component.test.ts +44 -0
- package/src/modes/components/status-line/component.ts +9 -1
- package/src/modes/components/status-line/segments.ts +3 -3
- package/src/modes/components/usage-row.ts +16 -2
- package/src/modes/controllers/command-controller.ts +6 -21
- package/src/modes/controllers/event-controller.ts +11 -9
- package/src/modes/controllers/extension-ui-controller.ts +84 -3
- package/src/modes/controllers/input-controller.ts +16 -13
- package/src/modes/controllers/selector-controller.ts +3 -8
- package/src/modes/controllers/streaming-reveal.ts +75 -53
- package/src/modes/controllers/tool-args-reveal.ts +340 -10
- package/src/modes/interactive-mode.ts +122 -79
- package/src/modes/rpc/rpc-client.ts +21 -0
- package/src/modes/rpc/rpc-mode.ts +197 -46
- package/src/modes/session-teardown.test.ts +219 -0
- package/src/modes/session-teardown.ts +82 -0
- package/src/modes/setup-wizard/scenes/theme.ts +2 -2
- package/src/modes/skill-command.ts +7 -20
- package/src/modes/theme/theme.ts +29 -15
- package/src/modes/types.ts +2 -3
- package/src/modes/utils/copy-targets.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +19 -2
- package/src/plan-mode/approved-plan-prompt.test.ts +36 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/agents/tester.md +6 -2
- package/src/prompts/skills/autoload.md +8 -0
- package/src/prompts/skills/user-invocation.md +11 -0
- package/src/prompts/steering/parent-irc.md +5 -0
- package/src/prompts/system/irc-incoming.md +2 -0
- package/src/prompts/system/mid-run-todo-nudge.md +3 -0
- package/src/prompts/system/plan-mode-approved.md +7 -10
- package/src/prompts/system/plan-mode-compact-instructions.md +2 -1
- package/src/prompts/system/plan-mode-reference.md +3 -4
- package/src/prompts/system/rewind-report.md +6 -0
- package/src/prompts/system/subagent-system-prompt.md +3 -0
- package/src/prompts/tools/irc.md +2 -1
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/rewind.md +3 -2
- package/src/prompts/tools/ssh.md +1 -0
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +18 -4
- package/src/session/agent-session.ts +660 -114
- package/src/session/exit-diagnostics.ts +202 -0
- package/src/session/session-context.ts +25 -13
- package/src/session/session-loader.ts +58 -25
- package/src/session/session-manager.ts +0 -1
- package/src/session/session-persistence.ts +30 -4
- package/src/session/settings-stream-fn.ts +14 -0
- package/src/slash-commands/builtin-registry.ts +35 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/executor.ts +31 -8
- package/src/task/index.ts +31 -9
- package/src/task/isolation-runner.ts +18 -2
- package/src/task/parallel.ts +7 -2
- package/src/task/render.test.ts +121 -0
- package/src/task/render.ts +48 -2
- package/src/task/worktree.ts +12 -13
- package/src/task/yield-assembly.ts +8 -5
- package/src/thinking.ts +5 -0
- package/src/tools/ask.ts +188 -9
- package/src/tools/ast-edit.ts +7 -0
- package/src/tools/ast-grep.ts +11 -0
- package/src/tools/bash.ts +6 -30
- package/src/tools/checkpoint.ts +15 -1
- package/src/tools/eval-render.ts +15 -0
- package/src/tools/fetch.ts +82 -18
- package/src/tools/gh.ts +1 -1
- package/src/tools/grep.ts +45 -27
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +24 -9
- package/src/tools/output-meta.ts +50 -0
- package/src/tools/output-schema-validator.ts +152 -15
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +5 -3
- package/src/tools/ssh.ts +8 -1
- package/src/tools/yield.ts +41 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/git.ts +271 -29
- package/src/utils/thinking-display.ts +13 -0
- package/src/web/search/index.ts +9 -28
- package/src/web/search/provider.ts +26 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/types.ts +5 -1
package/src/task/index.ts
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
} from "./isolation-runner";
|
|
61
61
|
import { generateTaskName } from "./name-generator";
|
|
62
62
|
import { AgentOutputManager } from "./output-manager";
|
|
63
|
-
import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
|
|
63
|
+
import { mapWithConcurrencyLimit, normalizeConcurrencyLimit, Semaphore } from "./parallel";
|
|
64
64
|
import { renderResult, renderCall as renderTaskCall } from "./render";
|
|
65
65
|
import { repairTaskParams } from "./repair-args";
|
|
66
66
|
import { parseIsolationMode } from "./worktree";
|
|
@@ -208,7 +208,7 @@ function renderDescription(
|
|
|
208
208
|
defaultAgent: spawnPolicy.defaultAgent,
|
|
209
209
|
defaultAgentIsGeneric: spawnPolicy.defaultAgent === DEFAULT_SPAWN_AGENT,
|
|
210
210
|
allowedAgentsText: spawnPolicy.allowedPromptText,
|
|
211
|
-
MAX_CONCURRENCY: maxConcurrency,
|
|
211
|
+
MAX_CONCURRENCY: normalizeConcurrencyLimit(maxConcurrency),
|
|
212
212
|
isolationEnabled,
|
|
213
213
|
batchEnabled,
|
|
214
214
|
asyncEnabled,
|
|
@@ -501,8 +501,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
501
501
|
readonly #blockedAgent: string | undefined;
|
|
502
502
|
/**
|
|
503
503
|
* One semaphore per TaskTool instance (i.e. per session): bounds concurrent
|
|
504
|
-
* subagents across parallel `task` calls within the session.
|
|
505
|
-
* `task.maxConcurrency`
|
|
504
|
+
* subagents across parallel `task` calls within the session. Resized in
|
|
505
|
+
* place from `task.maxConcurrency` before every acquire/release so a
|
|
506
|
+
* mid-session settings change (UI toggle, `/settings`) applies to both new
|
|
507
|
+
* spawns and work already parked in the semaphore queue.
|
|
506
508
|
*/
|
|
507
509
|
#spawnSemaphore: Semaphore | undefined;
|
|
508
510
|
|
|
@@ -545,10 +547,19 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
545
547
|
}
|
|
546
548
|
|
|
547
549
|
#getSpawnSemaphore(): Semaphore {
|
|
548
|
-
|
|
550
|
+
const max = this.session.settings.get("task.maxConcurrency");
|
|
551
|
+
if (this.#spawnSemaphore) {
|
|
552
|
+
this.#spawnSemaphore.resize(max);
|
|
553
|
+
} else {
|
|
554
|
+
this.#spawnSemaphore = new Semaphore(max);
|
|
555
|
+
}
|
|
549
556
|
return this.#spawnSemaphore;
|
|
550
557
|
}
|
|
551
558
|
|
|
559
|
+
#releaseSpawnSemaphore(): void {
|
|
560
|
+
this.#getSpawnSemaphore().release();
|
|
561
|
+
}
|
|
562
|
+
|
|
552
563
|
/**
|
|
553
564
|
* Create a TaskTool instance with async agent discovery.
|
|
554
565
|
*/
|
|
@@ -803,6 +814,17 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
803
814
|
const startedAt = Date.now();
|
|
804
815
|
const semaphore = this.#getSpawnSemaphore();
|
|
805
816
|
let semaphoreHeld = false;
|
|
817
|
+
// Every release funnels through here: the flag flips before the
|
|
818
|
+
// release so no path — acquire-time abort, executor failure, or a
|
|
819
|
+
// future refactor that reorders the branches — can return a permit
|
|
820
|
+
// twice. Releasing a permit this job never acquired would steal one
|
|
821
|
+
// from a running job and let a later spawn start past
|
|
822
|
+
// task.maxConcurrency.
|
|
823
|
+
const releasePermit = () => {
|
|
824
|
+
if (!semaphoreHeld) return;
|
|
825
|
+
semaphoreHeld = false;
|
|
826
|
+
this.#releaseSpawnSemaphore();
|
|
827
|
+
};
|
|
806
828
|
try {
|
|
807
829
|
await semaphore.acquire(runSignal);
|
|
808
830
|
semaphoreHeld = true;
|
|
@@ -814,7 +836,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
814
836
|
}
|
|
815
837
|
const acquiredAt = Date.now();
|
|
816
838
|
if (!semaphoreHeld || runSignal.aborted) {
|
|
817
|
-
|
|
839
|
+
releasePermit();
|
|
818
840
|
progress.status = "aborted";
|
|
819
841
|
onSettled?.(true);
|
|
820
842
|
throw new Error("Aborted before execution");
|
|
@@ -886,7 +908,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
886
908
|
const hint = AgentRegistry.global().get(agentId) ? buildFollowUpHint(false) : "";
|
|
887
909
|
throw new TaskJobError(`${message}${hint}`);
|
|
888
910
|
} finally {
|
|
889
|
-
|
|
911
|
+
releasePermit();
|
|
890
912
|
}
|
|
891
913
|
},
|
|
892
914
|
{
|
|
@@ -931,7 +953,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
931
953
|
{ invokedAt, acquiredAt },
|
|
932
954
|
);
|
|
933
955
|
} finally {
|
|
934
|
-
|
|
956
|
+
this.#releaseSpawnSemaphore();
|
|
935
957
|
}
|
|
936
958
|
}
|
|
937
959
|
|
|
@@ -979,7 +1001,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
979
1001
|
{ invokedAt, acquiredAt },
|
|
980
1002
|
);
|
|
981
1003
|
} finally {
|
|
982
|
-
|
|
1004
|
+
this.#releaseSpawnSemaphore();
|
|
983
1005
|
}
|
|
984
1006
|
},
|
|
985
1007
|
signal,
|
|
@@ -291,15 +291,31 @@ export async function mergeIsolatedChanges(opts: IsolationMergeOptions): Promise
|
|
|
291
291
|
hadAnyChanges = false;
|
|
292
292
|
} else {
|
|
293
293
|
const normalized = patchText.endsWith("\n") ? patchText : `${patchText}\n`;
|
|
294
|
-
|
|
294
|
+
// Idempotence: declare a no-op only when the reverse patch applies AND
|
|
295
|
+
// the forward patch does not. `--reverse --check` alone can theoretically
|
|
296
|
+
// succeed if the file happens to carry the postimage at another location
|
|
297
|
+
// via git-apply's fuzz factor; requiring the forward check to fail
|
|
298
|
+
// removes that ambiguity while still catching true already-applied
|
|
299
|
+
// runs. Reads only — neither call touches the worktree, unlike
|
|
300
|
+
// `--3way --check`, which exits 0 even when the real apply would
|
|
301
|
+
// leave conflict markers and unmerged index entries.
|
|
302
|
+
const [alreadyApplied, forwardApplies] = await Promise.all([
|
|
303
|
+
git.patch.canApplyText(repoRoot, normalized, { reverse: true }),
|
|
304
|
+
git.patch.canApplyText(repoRoot, normalized),
|
|
305
|
+
]);
|
|
295
306
|
hadAnyChanges = false;
|
|
296
|
-
if (
|
|
307
|
+
if (alreadyApplied && !forwardApplies) {
|
|
308
|
+
changesApplied = true;
|
|
309
|
+
} else if (forwardApplies) {
|
|
310
|
+
changesApplied = true;
|
|
297
311
|
try {
|
|
298
312
|
await git.patch.applyText(repoRoot, normalized);
|
|
299
313
|
hadAnyChanges = true;
|
|
300
314
|
} catch {
|
|
301
315
|
changesApplied = false;
|
|
302
316
|
}
|
|
317
|
+
} else {
|
|
318
|
+
changesApplied = false;
|
|
303
319
|
}
|
|
304
320
|
}
|
|
305
321
|
}
|
package/src/task/parallel.ts
CHANGED
|
@@ -90,13 +90,18 @@ export async function mapWithConcurrencyLimit<T, R>(
|
|
|
90
90
|
* immediately — matching `task.maxConcurrency = 0`'s "Unlimited" semantics in the
|
|
91
91
|
* settings UI ([#3305](https://github.com/can1357/oh-my-pi/issues/3305)).
|
|
92
92
|
*/
|
|
93
|
+
export function normalizeConcurrencyLimit(max: number): number {
|
|
94
|
+
const normalizedMax = Number.isFinite(max) ? Math.trunc(max) : 0;
|
|
95
|
+
return normalizedMax > 0 ? normalizedMax : 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
93
98
|
export class Semaphore {
|
|
94
99
|
#max: number;
|
|
95
100
|
#current = 0;
|
|
96
101
|
#queue: Array<() => void> = [];
|
|
97
102
|
|
|
98
103
|
constructor(max: number) {
|
|
99
|
-
const normalizedMax =
|
|
104
|
+
const normalizedMax = normalizeConcurrencyLimit(max);
|
|
100
105
|
this.#max = normalizedMax > 0 ? normalizedMax : Number.POSITIVE_INFINITY;
|
|
101
106
|
}
|
|
102
107
|
|
|
@@ -155,7 +160,7 @@ export class Semaphore {
|
|
|
155
160
|
* never push concurrency past the cap (issue #3464 review feedback).
|
|
156
161
|
*/
|
|
157
162
|
resize(max: number): void {
|
|
158
|
-
const normalizedMax =
|
|
163
|
+
const normalizedMax = normalizeConcurrencyLimit(max);
|
|
159
164
|
this.#max = normalizedMax > 0 ? normalizedMax : Number.POSITIVE_INFINITY;
|
|
160
165
|
while (this.#current < this.#max) {
|
|
161
166
|
const next = this.#queue.shift();
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, it } from "bun:test";
|
|
2
|
+
import { Settings } from "../config/settings";
|
|
3
|
+
import { getThemeByName, setThemeInstance, type Theme } from "../modes/theme/theme";
|
|
4
|
+
import { renderResult } from "./render";
|
|
5
|
+
import type { AgentProgress, TaskToolDetails } from "./types";
|
|
6
|
+
|
|
7
|
+
const strip = (lines: readonly string[]): string =>
|
|
8
|
+
lines
|
|
9
|
+
.join("\n")
|
|
10
|
+
.replace(/\x1b\]8;[^\x1b\x07]*(?:\x07|\x1b\\)/g, "")
|
|
11
|
+
.replace(/\x1b\[[0-9;]*m/g, "");
|
|
12
|
+
|
|
13
|
+
const originalRowsDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "rows");
|
|
14
|
+
|
|
15
|
+
function setViewportRows(rows: number): void {
|
|
16
|
+
Object.defineProperty(process.stdout, "rows", { configurable: true, value: rows });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function restoreViewportRows(): void {
|
|
20
|
+
if (originalRowsDescriptor) {
|
|
21
|
+
Object.defineProperty(process.stdout, "rows", originalRowsDescriptor);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
Reflect.deleteProperty(process.stdout, "rows");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeProgress(recentOutput: string[]): AgentProgress {
|
|
28
|
+
return {
|
|
29
|
+
index: 0,
|
|
30
|
+
id: "NoisySubagent",
|
|
31
|
+
agent: "task",
|
|
32
|
+
agentSource: "bundled",
|
|
33
|
+
status: "running",
|
|
34
|
+
task: "produce noisy output",
|
|
35
|
+
recentTools: [],
|
|
36
|
+
recentOutput,
|
|
37
|
+
toolCount: 1,
|
|
38
|
+
requests: 1,
|
|
39
|
+
tokens: 0,
|
|
40
|
+
cost: 0,
|
|
41
|
+
durationMs: 0,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderProgressText(progress: AgentProgress, expanded: boolean, uiTheme: Theme): string {
|
|
46
|
+
const details: TaskToolDetails = {
|
|
47
|
+
projectAgentsDir: null,
|
|
48
|
+
results: [],
|
|
49
|
+
totalDurationMs: 1,
|
|
50
|
+
progress: [progress],
|
|
51
|
+
};
|
|
52
|
+
const component = renderResult(
|
|
53
|
+
{ content: [{ type: "text", text: "Running 1 agent..." }], details },
|
|
54
|
+
{ expanded, isPartial: true },
|
|
55
|
+
uiTheme,
|
|
56
|
+
);
|
|
57
|
+
return strip(component.render(120));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("task live progress rendering", () => {
|
|
61
|
+
let uiTheme: Theme;
|
|
62
|
+
|
|
63
|
+
beforeAll(async () => {
|
|
64
|
+
await Settings.init({ inMemory: true });
|
|
65
|
+
const loaded = await getThemeByName("dark");
|
|
66
|
+
if (!loaded) throw new Error("theme unavailable");
|
|
67
|
+
uiTheme = loaded;
|
|
68
|
+
setThemeInstance(uiTheme);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
restoreViewportRows();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("caps subagent recent output with the viewport budget instead of a fixed six lines", () => {
|
|
76
|
+
setViewportRows(40);
|
|
77
|
+
const chronological = Array.from({ length: 8 }, (_, index) => `line ${index + 1}`);
|
|
78
|
+
const text = renderProgressText(makeProgress([...chronological].reverse()), true, uiTheme);
|
|
79
|
+
|
|
80
|
+
expect(text).toContain("line 1");
|
|
81
|
+
expect(text).toContain("line 8");
|
|
82
|
+
expect(text).not.toContain("more lines");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("keeps the newest subagent output when the viewport cap truncates", () => {
|
|
86
|
+
setViewportRows(24);
|
|
87
|
+
const chronological = Array.from({ length: 8 }, (_, index) => `line ${index + 1}`);
|
|
88
|
+
const text = renderProgressText(makeProgress([...chronological].reverse()), true, uiTheme);
|
|
89
|
+
|
|
90
|
+
expect(text).toContain("… 3 earlier lines");
|
|
91
|
+
expect(text).not.toContain("line 1");
|
|
92
|
+
expect(text).not.toContain("line 3");
|
|
93
|
+
expect(text).toContain("line 4");
|
|
94
|
+
expect(text).toContain("line 8");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("strips bash footer notices from expanded subagent recent output", () => {
|
|
98
|
+
setViewportRows(40);
|
|
99
|
+
const chronological = [
|
|
100
|
+
"line 1",
|
|
101
|
+
"line 2",
|
|
102
|
+
"line 3",
|
|
103
|
+
"line 4",
|
|
104
|
+
"line 5",
|
|
105
|
+
"line 6",
|
|
106
|
+
"Wall time: 0.03 seconds",
|
|
107
|
+
"[raw output: artifact://123]",
|
|
108
|
+
];
|
|
109
|
+
const progress = makeProgress([...chronological].reverse());
|
|
110
|
+
|
|
111
|
+
const expandedText = renderProgressText(progress, true, uiTheme);
|
|
112
|
+
expect(expandedText).toContain("line 1");
|
|
113
|
+
expect(expandedText).toContain("line 6");
|
|
114
|
+
expect(expandedText).not.toContain("Wall time:");
|
|
115
|
+
expect(expandedText).not.toContain("raw output:");
|
|
116
|
+
|
|
117
|
+
const collapsedText = renderProgressText(progress, false, uiTheme);
|
|
118
|
+
expect(collapsedText).not.toContain("line 1");
|
|
119
|
+
expect(collapsedText).not.toContain("raw output:");
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/task/render.ts
CHANGED
|
@@ -12,13 +12,16 @@ import { settings } from "../config/settings";
|
|
|
12
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
13
13
|
import { formatContextUsage } from "../modes/components/status-line/context-thresholds";
|
|
14
14
|
import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
15
|
+
import { stripGeneratedOutputNotice, stripRawOutputArtifactNotice } from "../tools/output-meta";
|
|
15
16
|
import {
|
|
17
|
+
capPreviewLines,
|
|
16
18
|
formatBadge,
|
|
17
19
|
formatDuration,
|
|
18
20
|
formatExpandHint,
|
|
19
21
|
formatMoreItems,
|
|
20
22
|
formatStatusIcon,
|
|
21
23
|
previewLine,
|
|
24
|
+
previewWindowRows,
|
|
22
25
|
replaceTabs,
|
|
23
26
|
type ToolUIStatus,
|
|
24
27
|
truncateToWidth,
|
|
@@ -450,6 +453,41 @@ function renderJsonTreeLines(
|
|
|
450
453
|
return { lines, truncated };
|
|
451
454
|
}
|
|
452
455
|
|
|
456
|
+
const BASH_WALL_TIME_NOTICE_RE = /^Wall time: \d+(?:\.\d+)? seconds$/u;
|
|
457
|
+
const BASH_EXIT_CODE_NOTICE_RE = /^Command exited with code -?\d+$/u;
|
|
458
|
+
|
|
459
|
+
function stripRecentOutputNoticeLine(text: string): string {
|
|
460
|
+
const trimmed = text.trimEnd();
|
|
461
|
+
const lineStart = trimmed.lastIndexOf("\n");
|
|
462
|
+
const candidateStart = lineStart === -1 ? 0 : lineStart + 1;
|
|
463
|
+
const line = trimmed.slice(candidateStart);
|
|
464
|
+
if (!BASH_WALL_TIME_NOTICE_RE.test(line) && !BASH_EXIT_CODE_NOTICE_RE.test(line)) return text;
|
|
465
|
+
return trimmed.slice(0, lineStart === -1 ? 0 : lineStart).trimEnd();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function sanitizeRecentOutput(output: string): string {
|
|
469
|
+
let text = output.trimEnd();
|
|
470
|
+
while (text) {
|
|
471
|
+
const withoutArtifactNotice = stripRawOutputArtifactNotice(text).text;
|
|
472
|
+
if (withoutArtifactNotice !== text) {
|
|
473
|
+
text = withoutArtifactNotice;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const withoutOutputNotice = stripGeneratedOutputNotice(text);
|
|
477
|
+
if (withoutOutputNotice !== text) {
|
|
478
|
+
text = withoutOutputNotice;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const withoutRuntimeNotice = stripRecentOutputNoticeLine(text);
|
|
482
|
+
if (withoutRuntimeNotice !== text) {
|
|
483
|
+
text = withoutRuntimeNotice;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
return text;
|
|
489
|
+
}
|
|
490
|
+
|
|
453
491
|
function renderOutputSection(
|
|
454
492
|
output: string,
|
|
455
493
|
continuePrefix: string,
|
|
@@ -1016,8 +1054,16 @@ function renderAgentProgress(
|
|
|
1016
1054
|
|
|
1017
1055
|
// Expanded view: recent output and tools
|
|
1018
1056
|
if (expanded && progress.status === "running") {
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1057
|
+
const previewRows = previewWindowRows();
|
|
1058
|
+
const output = capPreviewLines(
|
|
1059
|
+
sanitizeRecentOutput([...progress.recentOutput].reverse().join("\n")).split("\n"),
|
|
1060
|
+
theme,
|
|
1061
|
+
{
|
|
1062
|
+
max: previewRows,
|
|
1063
|
+
expandHint: false,
|
|
1064
|
+
},
|
|
1065
|
+
).join("\n");
|
|
1066
|
+
lines.push(...renderOutputSection(output, continuePrefix, expanded, theme, 2, previewRows));
|
|
1021
1067
|
}
|
|
1022
1068
|
|
|
1023
1069
|
return lines;
|
package/src/task/worktree.ts
CHANGED
|
@@ -305,16 +305,13 @@ export async function applyNestedPatches(
|
|
|
305
305
|
}
|
|
306
306
|
} finally {
|
|
307
307
|
if (stashed) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
} catch (popErr) {
|
|
311
|
-
const message = popErr instanceof Error ? popErr.message : String(popErr);
|
|
308
|
+
const restored = await git.stash.tryPop(nestedDir, { index: true });
|
|
309
|
+
if (!restored) {
|
|
312
310
|
logger.warn("Pre-existing nested-repo dirty state could not be auto-restored", {
|
|
313
311
|
nestedDir,
|
|
314
|
-
error: message,
|
|
315
312
|
});
|
|
316
313
|
warnings.push(
|
|
317
|
-
`Pre-existing dirty state in nested repo \`${relativePath}\` could not be auto-restored after the agent commit; stash entry preserved
|
|
314
|
+
`Pre-existing dirty state in nested repo \`${relativePath}\` could not be auto-restored after the agent commit; stash entry preserved.`,
|
|
318
315
|
);
|
|
319
316
|
}
|
|
320
317
|
}
|
|
@@ -755,13 +752,15 @@ export async function mergeTaskBranches(
|
|
|
755
752
|
}
|
|
756
753
|
} finally {
|
|
757
754
|
if (didStash) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
// the
|
|
764
|
-
//
|
|
755
|
+
const restored = await git.stash.tryPop(repoRoot, { index: true });
|
|
756
|
+
if (!restored) {
|
|
757
|
+
// Stash pop would leave stage 1/2/3 unmerged entries in `.git/index`
|
|
758
|
+
// that overlay-isolated subsequent tasks inherit through the lower
|
|
759
|
+
// layer, corrupting every downstream `captureRepoDeltaPatch`. `tryPop`
|
|
760
|
+
// short-circuits the pop when the WIP would conflict with the
|
|
761
|
+
// cherry-picked HEAD (and reset-cleans up if a rarer conflict slips
|
|
762
|
+
// past). The merged branches DID land — surface a stash-restore
|
|
763
|
+
// warning without claiming the merge failed.
|
|
765
764
|
logger.warn("Failed to restore stashed changes after task merge; stash entry preserved");
|
|
766
765
|
const stashConflict =
|
|
767
766
|
"stash pop: cherry-picked changes conflict with uncommitted edits. The merged commits are on HEAD; run `git stash pop` and resolve manually.";
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* `session-manager`, the TUI tool renderers). It has no side effects and
|
|
9
9
|
* depends only on the yield type and the output-schema validator.
|
|
10
10
|
*/
|
|
11
|
+
import { dereferenceJsonSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
12
|
+
import { isRecord } from "@oh-my-pi/pi-utils";
|
|
11
13
|
import { buildOutputValidator } from "../tools/output-schema-validator";
|
|
12
14
|
import type { YieldItem } from "./types";
|
|
13
15
|
|
|
@@ -105,11 +107,12 @@ export function arrayValuedLabels(outputSchema: unknown): ReadonlySet<string> {
|
|
|
105
107
|
// with `type: "array"`, which raw `normalizeSchema` would not expose.
|
|
106
108
|
const { jsonSchema } = buildOutputValidator(outputSchema);
|
|
107
109
|
if (jsonSchema === undefined) return labels;
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
const dereferenced = dereferenceJsonSchema(jsonSchema);
|
|
111
|
+
const labelSchema = isRecord(dereferenced) ? dereferenced : jsonSchema;
|
|
112
|
+
const properties = labelSchema.properties;
|
|
113
|
+
if (!isRecord(properties)) return labels;
|
|
114
|
+
for (const key in properties) {
|
|
115
|
+
if (isArrayTypedSchema(properties[key])) labels.add(key);
|
|
113
116
|
}
|
|
114
117
|
return labels;
|
|
115
118
|
}
|
package/src/thinking.ts
CHANGED
|
@@ -124,6 +124,11 @@ export const AUTO_THINKING = "auto" as const;
|
|
|
124
124
|
/** A thinking selector as configured by the user — a concrete level or `auto`. */
|
|
125
125
|
export type ConfiguredThinkingLevel = ThinkingLevel | typeof AUTO_THINKING;
|
|
126
126
|
|
|
127
|
+
/** Maps the session-level `auto` sentinel to `undefined`; concrete levels pass through. */
|
|
128
|
+
export function concreteThinkingLevel(level: ConfiguredThinkingLevel | undefined): ThinkingLevel | undefined {
|
|
129
|
+
return level === AUTO_THINKING ? undefined : level;
|
|
130
|
+
}
|
|
131
|
+
|
|
127
132
|
/** Metadata used to render the `auto` selector value alongside concrete levels. */
|
|
128
133
|
export interface ConfiguredThinkingLevelMetadata {
|
|
129
134
|
value: ConfiguredThinkingLevel;
|
package/src/tools/ask.ts
CHANGED
|
@@ -17,7 +17,17 @@
|
|
|
17
17
|
|
|
18
18
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
19
|
import type { ToolExample } from "@oh-my-pi/pi-ai";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type Component,
|
|
22
|
+
Ellipsis,
|
|
23
|
+
Markdown,
|
|
24
|
+
type MarkdownTheme,
|
|
25
|
+
renderInlineMarkdown,
|
|
26
|
+
TERMINAL,
|
|
27
|
+
Text,
|
|
28
|
+
truncateToWidth,
|
|
29
|
+
visibleWidth,
|
|
30
|
+
} from "@oh-my-pi/pi-tui";
|
|
21
31
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
22
32
|
import { type as arkType } from "arktype";
|
|
23
33
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -142,19 +152,146 @@ interface CustomInputContext {
|
|
|
142
152
|
markableCount: number;
|
|
143
153
|
}
|
|
144
154
|
|
|
155
|
+
/** Hard caps for the editor title rendered while the user types an `Other`
|
|
156
|
+
* custom answer. {@link HookEditorComponent} renders the title via a single
|
|
157
|
+
* `Text` child stacked above the prompt editor with no `maxVisible` windowing,
|
|
158
|
+
* so the title MUST fit a normal terminal:
|
|
159
|
+
* - {@link MAX_CUSTOM_INPUT_OPTION_ROWS}: at most this many option-row entries
|
|
160
|
+
* survive {@link pickCustomInputOptionWindow}, regardless of total options.
|
|
161
|
+
* - {@link MAX_CUSTOM_INPUT_TITLE_ROWS}: hard cap on rendered title rows after
|
|
162
|
+
* every line is pre-truncated to one row at the live terminal width. Sized
|
|
163
|
+
* so a 24-row terminal still has space for the input row, hint, and chrome.
|
|
164
|
+
*/
|
|
165
|
+
const MAX_CUSTOM_INPUT_OPTION_ROWS = 8;
|
|
166
|
+
const MAX_CUSTOM_INPUT_TITLE_ROWS = 16;
|
|
167
|
+
const MIN_CUSTOM_INPUT_CONTENT_WIDTH = 20;
|
|
168
|
+
/** Subtracted from the terminal width to leave room for the surrounding
|
|
169
|
+
* `Text(... padX=1)` padding + DynamicBorder vertical chrome. */
|
|
170
|
+
const CUSTOM_INPUT_CHROME_COLUMNS = 4;
|
|
171
|
+
const CUSTOM_INPUT_DESCRIPTION_INDENT = " ";
|
|
172
|
+
|
|
173
|
+
function customInputContentWidth(): number {
|
|
174
|
+
const cols = process.stdout.columns ?? 80;
|
|
175
|
+
return Math.max(MIN_CUSTOM_INPUT_CONTENT_WIDTH, cols - CUSTOM_INPUT_CHROME_COLUMNS);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function clampLineToWidth(line: string, width: number): string {
|
|
179
|
+
if (visibleWidth(line) <= width) return line;
|
|
180
|
+
return truncateToWidth(line, width, Ellipsis.Unicode);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function flattenDescription(text: string): string {
|
|
184
|
+
return text.replace(/\s+/g, " ").trim();
|
|
185
|
+
}
|
|
186
|
+
|
|
145
187
|
function getSelectOptionDescription(option: ExtensionUISelectItem): string | undefined {
|
|
146
188
|
return typeof option === "string" ? undefined : option.description;
|
|
147
189
|
}
|
|
148
190
|
|
|
149
|
-
|
|
191
|
+
interface CustomInputOptionGap {
|
|
192
|
+
total: number;
|
|
193
|
+
checked: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface CustomInputOptionWindow {
|
|
197
|
+
indices: number[];
|
|
198
|
+
gapBefore: Map<number, CustomInputOptionGap>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Window the option list so the title stays bounded. Required rows are the
|
|
202
|
+
* selected `Other` row and the first option as an anchor; checked rows fill
|
|
203
|
+
* the remaining budget before unselected leading rows. Hidden checked options
|
|
204
|
+
* are summarized in gap markers so the rendered option-row count still never
|
|
205
|
+
* exceeds {@link MAX_CUSTOM_INPUT_OPTION_ROWS}. */
|
|
206
|
+
function pickCustomInputOptionWindow(
|
|
207
|
+
total: number,
|
|
208
|
+
selectedIndex: number,
|
|
209
|
+
checked: ReadonlySet<number>,
|
|
210
|
+
): CustomInputOptionWindow {
|
|
211
|
+
if (total === 0) return { indices: [], gapBefore: new Map() };
|
|
212
|
+
if (total <= MAX_CUSTOM_INPUT_OPTION_ROWS) {
|
|
213
|
+
return {
|
|
214
|
+
indices: Array.from({ length: total }, (_, i) => i),
|
|
215
|
+
gapBefore: new Map(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const keep = new Set<number>();
|
|
219
|
+
const addIfRoom = (index: number) => {
|
|
220
|
+
if (index >= 0 && index < total && keep.size < MAX_CUSTOM_INPUT_OPTION_ROWS) {
|
|
221
|
+
keep.add(index);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
addIfRoom(selectedIndex);
|
|
225
|
+
addIfRoom(0);
|
|
226
|
+
for (const i of [...checked].sort((a, b) => a - b)) {
|
|
227
|
+
addIfRoom(i);
|
|
228
|
+
}
|
|
229
|
+
for (let i = 0; i < total && keep.size < MAX_CUSTOM_INPUT_OPTION_ROWS; i++) {
|
|
230
|
+
addIfRoom(i);
|
|
231
|
+
}
|
|
232
|
+
const indices = [...keep].sort((a, b) => a - b);
|
|
233
|
+
const gapBefore = new Map<number, CustomInputOptionGap>();
|
|
234
|
+
const countCheckedBetween = (startInclusive: number, endExclusive: number): number => {
|
|
235
|
+
let count = 0;
|
|
236
|
+
for (const i of checked) {
|
|
237
|
+
if (i >= startInclusive && i < endExclusive) count++;
|
|
238
|
+
}
|
|
239
|
+
return count;
|
|
240
|
+
};
|
|
241
|
+
let prev = -1;
|
|
242
|
+
for (const idx of indices) {
|
|
243
|
+
if (idx > prev + 1) {
|
|
244
|
+
gapBefore.set(idx, {
|
|
245
|
+
total: idx - prev - 1,
|
|
246
|
+
checked: countCheckedBetween(prev + 1, idx),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
prev = idx;
|
|
250
|
+
}
|
|
251
|
+
if (prev < total - 1) {
|
|
252
|
+
gapBefore.set(total, {
|
|
253
|
+
total: total - 1 - prev,
|
|
254
|
+
checked: countCheckedBetween(prev + 1, total),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return { indices, gapBefore };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface CustomInputRow {
|
|
261
|
+
text: string;
|
|
262
|
+
/** Lower priority drops first when over budget; negative values are pinned.
|
|
263
|
+
* Gap markers are budgeted rows too so sparse checked selections cannot
|
|
264
|
+
* push the editor input off-screen. */
|
|
265
|
+
priority: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function buildCustomInputRows(
|
|
150
269
|
question: string,
|
|
151
270
|
options: ExtensionUISelectItem[],
|
|
152
271
|
context: CustomInputContext,
|
|
153
|
-
|
|
272
|
+
contentWidth: number,
|
|
273
|
+
): CustomInputRow[] {
|
|
154
274
|
const selectedIndex = options.findIndex(option => getSelectOptionLabel(option) === OTHER_OPTION);
|
|
155
|
-
const lines = [question, ""];
|
|
156
275
|
const checked = new Set(context.checkedIndices ?? []);
|
|
157
|
-
|
|
276
|
+
const window = pickCustomInputOptionWindow(options.length, selectedIndex, checked);
|
|
277
|
+
const rows: CustomInputRow[] = [];
|
|
278
|
+
rows.push({ text: clampLineToWidth(question, contentWidth), priority: -1 });
|
|
279
|
+
rows.push({ text: "", priority: -1 });
|
|
280
|
+
|
|
281
|
+
const emitGap = (gap: CustomInputOptionGap) => {
|
|
282
|
+
const checkedSuffix = gap.checked > 0 ? `, ${gap.checked} checked` : "";
|
|
283
|
+
rows.push({
|
|
284
|
+
text: clampLineToWidth(
|
|
285
|
+
` … ${gap.total} more option${gap.total === 1 ? "" : "s"}${checkedSuffix} …`,
|
|
286
|
+
contentWidth,
|
|
287
|
+
),
|
|
288
|
+
priority: 2,
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
for (const index of window.indices) {
|
|
293
|
+
const gap = window.gapBefore.get(index);
|
|
294
|
+
if (gap !== undefined) emitGap(gap);
|
|
158
295
|
const option = options[index]!;
|
|
159
296
|
const label = getSelectOptionLabel(option);
|
|
160
297
|
const isSelected = index === selectedIndex;
|
|
@@ -167,12 +304,54 @@ function formatCustomInputTitle(
|
|
|
167
304
|
: isSelected
|
|
168
305
|
? `${theme.nav.cursor} `
|
|
169
306
|
: " ";
|
|
170
|
-
|
|
307
|
+
rows.push({ text: clampLineToWidth(prefix + label, contentWidth), priority: -1 });
|
|
171
308
|
const description = getSelectOptionDescription(option);
|
|
172
|
-
if (description)
|
|
309
|
+
if (description) {
|
|
310
|
+
const flat = flattenDescription(description);
|
|
311
|
+
if (flat) {
|
|
312
|
+
rows.push({
|
|
313
|
+
text: clampLineToWidth(`${CUSTOM_INPUT_DESCRIPTION_INDENT}${flat}`, contentWidth),
|
|
314
|
+
// Selected (Other) carries no description; favor checked rows
|
|
315
|
+
// when budget pressure forces description rows to be dropped.
|
|
316
|
+
priority: isSelected ? 2 : checked.has(index) ? 1 : 0,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
173
320
|
}
|
|
174
|
-
|
|
175
|
-
|
|
321
|
+
|
|
322
|
+
const trailingGap = window.gapBefore.get(options.length);
|
|
323
|
+
if (trailingGap !== undefined) emitGap(trailingGap);
|
|
324
|
+
rows.push({ text: "", priority: -1 });
|
|
325
|
+
rows.push({ text: "Enter your response:", priority: -1 });
|
|
326
|
+
return rows;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function applyCustomInputRowBudget(rows: CustomInputRow[], budget: number): CustomInputRow[] {
|
|
330
|
+
if (rows.length <= budget) return rows;
|
|
331
|
+
// Drop droppable rows lowest priority first; on ties, drop later rows first
|
|
332
|
+
// so the user still sees the earliest options' descriptions.
|
|
333
|
+
const droppable = rows
|
|
334
|
+
.map((row, index) => ({ row, index }))
|
|
335
|
+
.filter(entry => entry.row.priority >= 0)
|
|
336
|
+
.sort((a, b) => a.row.priority - b.row.priority || b.index - a.index);
|
|
337
|
+
const removed = new Set<number>();
|
|
338
|
+
for (const { index } of droppable) {
|
|
339
|
+
if (rows.length - removed.size <= budget) break;
|
|
340
|
+
removed.add(index);
|
|
341
|
+
}
|
|
342
|
+
return rows.filter((_, i) => !removed.has(i));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function formatCustomInputTitle(
|
|
346
|
+
question: string,
|
|
347
|
+
options: ExtensionUISelectItem[],
|
|
348
|
+
context: CustomInputContext,
|
|
349
|
+
): string {
|
|
350
|
+
const contentWidth = customInputContentWidth();
|
|
351
|
+
const rows = buildCustomInputRows(question, options, context, contentWidth);
|
|
352
|
+
return applyCustomInputRowBudget(rows, MAX_CUSTOM_INPUT_TITLE_ROWS)
|
|
353
|
+
.map(row => row.text)
|
|
354
|
+
.join("\n");
|
|
176
355
|
}
|
|
177
356
|
|
|
178
357
|
// =============================================================================
|