@oh-my-pi/pi-coding-agent 14.5.6 → 14.5.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 +26 -0
- package/package.json +7 -7
- package/src/config/model-registry.ts +23 -1
- package/src/config/settings-schema.ts +24 -1
- package/src/config/settings.ts +16 -0
- package/src/edit/modes/atom.ts +3 -5
- package/src/modes/components/hook-editor.ts +2 -2
- package/src/modes/components/settings-defs.ts +10 -0
- package/src/modes/components/status-line/presets.ts +7 -7
- package/src/modes/components/status-line/segments.ts +16 -10
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -1
- package/src/modes/components/status-line.ts +6 -0
- package/src/modes/controllers/event-controller.ts +14 -9
- package/src/modes/controllers/input-controller.ts +15 -0
- package/src/modes/interactive-mode.ts +72 -0
- package/src/modes/theme/defaults/dark-poimandres.json +1 -0
- package/src/modes/theme/defaults/light-poimandres.json +1 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +5 -0
- package/src/prompts/tools/run-command.md +16 -0
- package/src/slash-commands/builtin-registry.ts +10 -0
- package/src/tools/bash.ts +149 -115
- package/src/tools/index.ts +11 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/run-command/index.ts +80 -0
- package/src/tools/run-command/render.ts +18 -0
- package/src/tools/run-command/runner.ts +198 -0
- package/src/tools/run-command/runners/cargo.ts +131 -0
- package/src/tools/run-command/runners/index.ts +8 -0
- package/src/tools/run-command/runners/just.ts +73 -0
- package/src/tools/run-command/runners/make.ts +101 -0
- package/src/tools/run-command/runners/pkg.ts +195 -0
- package/src/tools/run-command/runners/task.ts +72 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.5.8] - 2026-04-29
|
|
6
|
+
### Breaking Changes
|
|
7
|
+
|
|
8
|
+
- Changed the task runner toggle from `just.enabled` to `runCommand.enabled`, so existing configurations using `just.enabled` must be migrated
|
|
9
|
+
- Removed the legacy `just` tool and replaced it with `run_command`
|
|
10
|
+
- Renamed the built-in tool API from `just` to `run_command`, so clients requesting/handling the old tool name must update
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added a new `run_command` tool that runs project tasks via a single `op` argument, auto-detecting and supporting recipes from justfiles, `package.json` scripts (including workspace packages), Cargo bin/example/test targets, Makefiles, and Taskfiles
|
|
15
|
+
- Added support for explicit runner-qualified tasks via `run_command` with `runnerId:task` syntax in the prompt guidance
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Changed automatic tool availability so requesting `bash` can now auto-include `run_command` when a supported task runner manifest is detected in the working directory
|
|
20
|
+
- Changed task resolution to disambiguate identical task names across multiple runners and show runner-aware command execution errors
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fixed editor draft being erased when a user message queued during streaming was eventually submitted; the queue/steer path now preserves any new prompt the user has typed since queuing, matching the existing optimistic-send protection.
|
|
25
|
+
|
|
26
|
+
## [14.5.7] - 2026-04-29
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Fixed hook editors to recognize Ctrl+Enter when terminals include NumLock or keypad Enter metadata.
|
|
5
31
|
## [14.5.6] - 2026-04-29
|
|
6
32
|
### Changed
|
|
7
33
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.5.
|
|
4
|
+
"version": "14.5.8",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.20.0",
|
|
48
48
|
"@mozilla/readability": "^0.6.0",
|
|
49
|
-
"@oh-my-pi/omp-stats": "14.5.
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "14.5.
|
|
51
|
-
"@oh-my-pi/pi-ai": "14.5.
|
|
52
|
-
"@oh-my-pi/pi-natives": "14.5.
|
|
53
|
-
"@oh-my-pi/pi-tui": "14.5.
|
|
54
|
-
"@oh-my-pi/pi-utils": "14.5.
|
|
49
|
+
"@oh-my-pi/omp-stats": "14.5.8",
|
|
50
|
+
"@oh-my-pi/pi-agent-core": "14.5.8",
|
|
51
|
+
"@oh-my-pi/pi-ai": "14.5.8",
|
|
52
|
+
"@oh-my-pi/pi-natives": "14.5.8",
|
|
53
|
+
"@oh-my-pi/pi-tui": "14.5.8",
|
|
54
|
+
"@oh-my-pi/pi-utils": "14.5.8",
|
|
55
55
|
"@puppeteer/browsers": "^2.13.0",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49",
|
|
57
57
|
"@xterm/headless": "^6.0.0",
|
|
@@ -249,7 +249,7 @@ const ProviderDiscoverySchema = Type.Object({
|
|
|
249
249
|
type: Type.Union([Type.Literal("ollama"), Type.Literal("llama.cpp"), Type.Literal("lm-studio")]),
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
-
const ProviderAuthSchema = Type.Union([Type.Literal("apiKey"), Type.Literal("none")]);
|
|
252
|
+
const ProviderAuthSchema = Type.Union([Type.Literal("apiKey"), Type.Literal("none"), Type.Literal("oauth")]);
|
|
253
253
|
|
|
254
254
|
const ProviderConfigSchema = Type.Object({
|
|
255
255
|
baseUrl: Type.Optional(Type.String({ minLength: 1 })),
|
|
@@ -643,6 +643,7 @@ type CustomModelOverlay = {
|
|
|
643
643
|
compat?: Model<Api>["compat"];
|
|
644
644
|
contextPromotionTarget?: string;
|
|
645
645
|
premiumMultiplier?: number;
|
|
646
|
+
isOAuth?: boolean;
|
|
646
647
|
};
|
|
647
648
|
|
|
648
649
|
function mergeCustomModelHeaders(
|
|
@@ -661,6 +662,22 @@ function mergeCustomModelHeaders(
|
|
|
661
662
|
return headers;
|
|
662
663
|
}
|
|
663
664
|
|
|
665
|
+
/**
|
|
666
|
+
* Decide whether a custom-yaml model should force OAuth-style request shaping.
|
|
667
|
+
* - Explicit `auth: oauth` → force on.
|
|
668
|
+
* - Explicit `auth: apiKey` / `auth: none` → leave unset (auto-detect by key prefix).
|
|
669
|
+
* - No `auth` specified and `api: anthropic-messages` → default on. Custom Anthropic
|
|
670
|
+
* endpoints are typically Claude-Code-style proxies (e.g. CLIProxyAPI) that expect
|
|
671
|
+
* the cloaked request shape regardless of how the proxy itself is authenticated.
|
|
672
|
+
* - Otherwise → unset.
|
|
673
|
+
*/
|
|
674
|
+
function resolveCustomModelIsOAuth(api: Api, providerAuth: ProviderAuthMode | undefined): boolean | undefined {
|
|
675
|
+
if (providerAuth === "oauth") return true;
|
|
676
|
+
if (providerAuth !== undefined) return undefined;
|
|
677
|
+
if (api === "anthropic-messages") return true;
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
|
|
664
681
|
function buildCustomModelOverlay(
|
|
665
682
|
providerName: string,
|
|
666
683
|
providerBaseUrl: string,
|
|
@@ -669,6 +686,7 @@ function buildCustomModelOverlay(
|
|
|
669
686
|
providerApiKey: string | undefined,
|
|
670
687
|
authHeader: boolean | undefined,
|
|
671
688
|
providerCompat: Model<Api>["compat"] | undefined,
|
|
689
|
+
providerAuth: ProviderAuthMode | undefined,
|
|
672
690
|
modelDef: CustomModelDefinitionLike,
|
|
673
691
|
): CustomModelOverlay | undefined {
|
|
674
692
|
const api = modelDef.api ?? providerApi;
|
|
@@ -689,6 +707,7 @@ function buildCustomModelOverlay(
|
|
|
689
707
|
compat: mergeCompat(providerCompat, modelDef.compat),
|
|
690
708
|
contextPromotionTarget: modelDef.contextPromotionTarget,
|
|
691
709
|
premiumMultiplier: modelDef.premiumMultiplier,
|
|
710
|
+
isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
|
|
692
711
|
};
|
|
693
712
|
}
|
|
694
713
|
|
|
@@ -720,6 +739,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
720
739
|
compat: resolvedModel.compat,
|
|
721
740
|
contextPromotionTarget: resolvedModel.contextPromotionTarget,
|
|
722
741
|
premiumMultiplier: resolvedModel.premiumMultiplier,
|
|
742
|
+
isOAuth: resolvedModel.isOAuth,
|
|
723
743
|
} as Model<Api>);
|
|
724
744
|
}
|
|
725
745
|
|
|
@@ -1735,6 +1755,7 @@ export class ModelRegistry {
|
|
|
1735
1755
|
providerConfig.apiKey,
|
|
1736
1756
|
providerConfig.authHeader,
|
|
1737
1757
|
providerConfig.compat,
|
|
1758
|
+
(providerConfig.auth as ProviderAuthMode | undefined) ?? undefined,
|
|
1738
1759
|
modelDef as CustomModelDefinitionLike,
|
|
1739
1760
|
);
|
|
1740
1761
|
if (!model) continue;
|
|
@@ -2069,6 +2090,7 @@ export class ModelRegistry {
|
|
|
2069
2090
|
config.apiKey,
|
|
2070
2091
|
config.authHeader,
|
|
2071
2092
|
config.compat,
|
|
2093
|
+
undefined,
|
|
2072
2094
|
modelDef as CustomModelDefinitionLike,
|
|
2073
2095
|
);
|
|
2074
2096
|
if (!overlay) {
|
|
@@ -59,7 +59,7 @@ export const TAB_METADATA: Record<SettingTab, { label: string; icon: `tab.${stri
|
|
|
59
59
|
export type StatusLineSegmentId =
|
|
60
60
|
| "pi"
|
|
61
61
|
| "model"
|
|
62
|
-
| "
|
|
62
|
+
| "mode"
|
|
63
63
|
| "path"
|
|
64
64
|
| "git"
|
|
65
65
|
| "pr"
|
|
@@ -608,6 +608,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
608
608
|
},
|
|
609
609
|
},
|
|
610
610
|
|
|
611
|
+
"loop.mode": {
|
|
612
|
+
type: "enum",
|
|
613
|
+
values: ["prompt", "compact", "reset"] as const,
|
|
614
|
+
default: "prompt",
|
|
615
|
+
ui: {
|
|
616
|
+
tab: "interaction",
|
|
617
|
+
label: "Loop Mode",
|
|
618
|
+
description: "What happens between /loop iterations before re-submitting the prompt",
|
|
619
|
+
submenu: true,
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
|
|
611
623
|
// Input and startup
|
|
612
624
|
doubleEscapeAction: {
|
|
613
625
|
type: "enum",
|
|
@@ -1294,6 +1306,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
1294
1306
|
},
|
|
1295
1307
|
},
|
|
1296
1308
|
|
|
1309
|
+
"runCommand.enabled": {
|
|
1310
|
+
type: "boolean",
|
|
1311
|
+
default: true,
|
|
1312
|
+
ui: {
|
|
1313
|
+
tab: "tools",
|
|
1314
|
+
label: "Run command",
|
|
1315
|
+
description:
|
|
1316
|
+
"Enable the run_command tool when a justfile / package.json / Cargo.toml / Makefile / Taskfile is present",
|
|
1317
|
+
},
|
|
1318
|
+
},
|
|
1319
|
+
|
|
1297
1320
|
"inspect_image.enabled": {
|
|
1298
1321
|
type: "boolean",
|
|
1299
1322
|
default: false,
|
package/src/config/settings.ts
CHANGED
|
@@ -532,6 +532,22 @@ export class Settings {
|
|
|
532
532
|
delete isolationObj.enabled;
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
// statusLine: rename "plan_mode" segment to "mode"
|
|
536
|
+
const statusLineObj = raw.statusLine as Record<string, unknown> | undefined;
|
|
537
|
+
if (statusLineObj) {
|
|
538
|
+
for (const key of ["leftSegments", "rightSegments"] as const) {
|
|
539
|
+
const segments = statusLineObj[key];
|
|
540
|
+
if (Array.isArray(segments)) {
|
|
541
|
+
statusLineObj[key] = segments.map(seg => (seg === "plan_mode" ? "mode" : seg));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const segmentOptions = statusLineObj.segmentOptions as Record<string, unknown> | undefined;
|
|
545
|
+
if (segmentOptions && "plan_mode" in segmentOptions && !("mode" in segmentOptions)) {
|
|
546
|
+
segmentOptions.mode = segmentOptions.plan_mode;
|
|
547
|
+
delete segmentOptions.plan_mode;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
535
551
|
return raw;
|
|
536
552
|
}
|
|
537
553
|
|
package/src/edit/modes/atom.ts
CHANGED
|
@@ -821,11 +821,9 @@ export function applyAtomEdits(text: string, edits: AtomEdit[]): AtomApplyResult
|
|
|
821
821
|
anchorMutated = true;
|
|
822
822
|
break;
|
|
823
823
|
case "delete":
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
);
|
|
828
|
-
}
|
|
824
|
+
// `-Lid|OLD` / `-Lid=OLD`: the OLD payload is informational only.
|
|
825
|
+
// The Lid hash already validates the line content (and auto-rebases
|
|
826
|
+
// when lines have shifted), so we ignore any OLD mismatch here.
|
|
829
827
|
replacement = [];
|
|
830
828
|
replacementSet = true;
|
|
831
829
|
anchorMutated = true;
|
|
@@ -104,8 +104,8 @@ export class HookEditorComponent extends Container {
|
|
|
104
104
|
|
|
105
105
|
/** Hook-style: Enter=newline, Ctrl+Enter=submit (original behavior) */
|
|
106
106
|
#handleHookStyleInput(keyData: string): void {
|
|
107
|
-
// Ctrl+Enter to submit
|
|
108
|
-
if (keyData
|
|
107
|
+
// Ctrl+Enter to submit. Use key matching so lock-key and keypad Enter variants work.
|
|
108
|
+
if (matchesKey(keyData, "ctrl+enter")) {
|
|
109
109
|
this.#onSubmitCallback(this.#editor.getText());
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
@@ -450,6 +450,16 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
450
450
|
{ value: "none", label: "None", description: "Space only" },
|
|
451
451
|
{ value: "ascii", label: "ASCII", description: "Greater-than signs" },
|
|
452
452
|
],
|
|
453
|
+
// Loop mode
|
|
454
|
+
"loop.mode": [
|
|
455
|
+
{
|
|
456
|
+
value: "prompt",
|
|
457
|
+
label: "Prompt",
|
|
458
|
+
description: "Re-submit the prompt as a follow-up message (current behavior)",
|
|
459
|
+
},
|
|
460
|
+
{ value: "compact", label: "Compact", description: "Compact the session context, then re-submit the prompt" },
|
|
461
|
+
{ value: "reset", label: "Reset", description: "Start a new session, then re-submit the prompt" },
|
|
462
|
+
],
|
|
453
463
|
};
|
|
454
464
|
|
|
455
465
|
function createSubmenuSettingDef(base: Omit<SettingDef, "type" | "options">, provider: OptionProvider): SettingDef {
|
|
@@ -2,7 +2,7 @@ import type { PresetDef, StatusLinePreset } from "./types";
|
|
|
2
2
|
|
|
3
3
|
export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
4
4
|
default: {
|
|
5
|
-
leftSegments: ["pi", "model", "
|
|
5
|
+
leftSegments: ["pi", "model", "mode", "path", "git", "pr", "context_pct", "token_total", "cost"],
|
|
6
6
|
rightSegments: ["session_name"],
|
|
7
7
|
separator: "powerline-thin",
|
|
8
8
|
segmentOptions: {
|
|
@@ -14,7 +14,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
14
14
|
|
|
15
15
|
minimal: {
|
|
16
16
|
leftSegments: ["path", "git"],
|
|
17
|
-
rightSegments: ["session_name", "
|
|
17
|
+
rightSegments: ["session_name", "mode", "context_pct"],
|
|
18
18
|
separator: "slash",
|
|
19
19
|
segmentOptions: {
|
|
20
20
|
path: { abbreviate: true, maxLength: 30 },
|
|
@@ -23,7 +23,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
23
23
|
},
|
|
24
24
|
|
|
25
25
|
compact: {
|
|
26
|
-
leftSegments: ["model", "
|
|
26
|
+
leftSegments: ["model", "mode", "git", "pr"],
|
|
27
27
|
rightSegments: ["session_name", "cost", "context_pct"],
|
|
28
28
|
separator: "powerline-thin",
|
|
29
29
|
segmentOptions: {
|
|
@@ -33,7 +33,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
33
33
|
},
|
|
34
34
|
|
|
35
35
|
full: {
|
|
36
|
-
leftSegments: ["pi", "hostname", "model", "
|
|
36
|
+
leftSegments: ["pi", "hostname", "model", "mode", "path", "git", "pr", "subagents"],
|
|
37
37
|
rightSegments: [
|
|
38
38
|
"session_name",
|
|
39
39
|
"token_in",
|
|
@@ -56,7 +56,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
56
56
|
|
|
57
57
|
nerd: {
|
|
58
58
|
// Full preset with all Nerd Font icons
|
|
59
|
-
leftSegments: ["pi", "hostname", "model", "
|
|
59
|
+
leftSegments: ["pi", "hostname", "model", "mode", "path", "git", "pr", "session", "subagents"],
|
|
60
60
|
rightSegments: [
|
|
61
61
|
"session_name",
|
|
62
62
|
"token_in",
|
|
@@ -81,7 +81,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
81
81
|
|
|
82
82
|
ascii: {
|
|
83
83
|
// No Nerd Font dependencies
|
|
84
|
-
leftSegments: ["model", "
|
|
84
|
+
leftSegments: ["model", "mode", "path", "git", "pr"],
|
|
85
85
|
rightSegments: ["session_name", "token_total", "cost", "context_pct"],
|
|
86
86
|
separator: "ascii",
|
|
87
87
|
segmentOptions: {
|
|
@@ -93,7 +93,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
93
93
|
|
|
94
94
|
custom: {
|
|
95
95
|
// User-defined - these are just defaults that get overridden
|
|
96
|
-
leftSegments: ["model", "
|
|
96
|
+
leftSegments: ["model", "mode", "path", "git", "pr"],
|
|
97
97
|
rightSegments: ["session_name", "token_total", "cost", "context_pct"],
|
|
98
98
|
separator: "powerline-thin",
|
|
99
99
|
segmentOptions: {},
|
|
@@ -76,18 +76,24 @@ const modelSegment: StatusLineSegment = {
|
|
|
76
76
|
},
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
const
|
|
80
|
-
id: "
|
|
79
|
+
const modeSegment: StatusLineSegment = {
|
|
80
|
+
id: "mode",
|
|
81
81
|
render(ctx) {
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
82
|
+
const plan = ctx.planMode;
|
|
83
|
+
if (plan && (plan.enabled || plan.paused)) {
|
|
84
|
+
const label = plan.paused ? "Plan ⏸" : "Plan";
|
|
85
|
+
const content = withIcon(theme.icon.plan, label);
|
|
86
|
+
const color = plan.paused ? "warning" : "accent";
|
|
87
|
+
return { content: theme.fg(color, content), visible: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const loop = ctx.loopMode;
|
|
91
|
+
if (loop?.enabled) {
|
|
92
|
+
const content = withIcon(theme.icon.loop, "Loop");
|
|
93
|
+
return { content: theme.fg("customMessageLabel", content), visible: true };
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
|
|
88
|
-
const content = withIcon(theme.icon.plan, label);
|
|
89
|
-
const color = status.paused ? "warning" : "accent";
|
|
90
|
-
return { content: theme.fg(color, content), visible: true };
|
|
96
|
+
return { content: "", visible: false };
|
|
91
97
|
},
|
|
92
98
|
};
|
|
93
99
|
|
|
@@ -375,7 +381,7 @@ const sessionNameSegment: StatusLineSegment = {
|
|
|
375
381
|
export const SEGMENTS: Record<StatusLineSegmentId, StatusLineSegment> = {
|
|
376
382
|
pi: piSegment,
|
|
377
383
|
model: modelSegment,
|
|
378
|
-
|
|
384
|
+
mode: modeSegment,
|
|
379
385
|
path: pathSegment,
|
|
380
386
|
git: gitSegment,
|
|
381
387
|
pr: prSegment,
|
|
@@ -18,7 +18,7 @@ import { ALL_SEGMENT_IDS } from "./status-line/segments";
|
|
|
18
18
|
const SEGMENT_INFO: Record<StatusLineSegmentId, { label: string; short: string }> = {
|
|
19
19
|
pi: { label: "Pi", short: "π icon" },
|
|
20
20
|
model: { label: "Model", short: "model name" },
|
|
21
|
-
|
|
21
|
+
mode: { label: "Mode", short: "plan/loop status" },
|
|
22
22
|
path: { label: "Path", short: "working dir" },
|
|
23
23
|
git: { label: "Git", short: "branch/status" },
|
|
24
24
|
pr: { label: "PR", short: "pull request" },
|
|
@@ -57,6 +57,7 @@ export class StatusLineComponent implements Component {
|
|
|
57
57
|
#subagentCount: number = 0;
|
|
58
58
|
#sessionStartTime: number = Date.now();
|
|
59
59
|
#planModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
60
|
+
#loopModeStatus: { enabled: boolean } | null = null;
|
|
60
61
|
|
|
61
62
|
// Git status caching (1s TTL)
|
|
62
63
|
#cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
|
|
@@ -102,6 +103,10 @@ export class StatusLineComponent implements Component {
|
|
|
102
103
|
this.#planModeStatus = status ?? null;
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
setLoopModeStatus(status: { enabled: boolean } | undefined): void {
|
|
107
|
+
this.#loopModeStatus = status ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
105
110
|
setHookStatus(key: string, text: string | undefined): void {
|
|
106
111
|
if (text === undefined) {
|
|
107
112
|
this.#hookStatuses.delete(key);
|
|
@@ -326,6 +331,7 @@ export class StatusLineComponent implements Component {
|
|
|
326
331
|
width,
|
|
327
332
|
options: this.#resolveSettings().segmentOptions ?? {},
|
|
328
333
|
planMode: this.#planModeStatus,
|
|
334
|
+
loopMode: this.#loopModeStatus,
|
|
329
335
|
usageStats,
|
|
330
336
|
contextPercent,
|
|
331
337
|
contextWindow,
|
|
@@ -174,18 +174,23 @@ export class EventController {
|
|
|
174
174
|
|
|
175
175
|
this.#resetReadGroup();
|
|
176
176
|
const wasOptimistic = this.ctx.optimisticUserMessageSignature === signature;
|
|
177
|
+
const wasLocallySubmitted = this.ctx.locallySubmittedUserSignatures.delete(signature) || wasOptimistic;
|
|
177
178
|
if (!wasOptimistic) {
|
|
178
179
|
this.ctx.addMessageToChat(event.message);
|
|
179
180
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
181
|
+
if (wasOptimistic) {
|
|
182
|
+
this.ctx.optimisticUserMessageSignature = undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Clear the editor only when the submission did not originate from a
|
|
186
|
+
// local submission (optimistic or queued-while-streaming). Both local
|
|
187
|
+
// paths already cleared the editor at submit time; clearing again here
|
|
188
|
+
// would race with the user typing the next prompt while the previous
|
|
189
|
+
// large redraw lands and erase their in-progress draft (#783).
|
|
190
|
+
if (!event.message.synthetic) {
|
|
191
|
+
if (!wasLocallySubmitted) {
|
|
192
|
+
this.ctx.editor.setText("");
|
|
193
|
+
}
|
|
189
194
|
this.ctx.updatePendingMessagesDisplay();
|
|
190
195
|
}
|
|
191
196
|
this.ctx.ui.requestRender();
|
|
@@ -44,6 +44,15 @@ export class InputController {
|
|
|
44
44
|
this.ctx.retryEscapeHandler,
|
|
45
45
|
);
|
|
46
46
|
this.ctx.editor.onEscape = () => {
|
|
47
|
+
if (this.ctx.loopModeEnabled) {
|
|
48
|
+
this.ctx.disableLoopMode();
|
|
49
|
+
if (this.ctx.session.isStreaming) {
|
|
50
|
+
void this.ctx.session.abort();
|
|
51
|
+
} else {
|
|
52
|
+
this.ctx.cancelPendingSubmission();
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
47
56
|
if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
|
|
48
57
|
return;
|
|
49
58
|
}
|
|
@@ -325,6 +334,11 @@ export class InputController {
|
|
|
325
334
|
this.ctx.editor.setText("");
|
|
326
335
|
const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
|
|
327
336
|
this.ctx.pendingImages = [];
|
|
337
|
+
// Record the signature so the queued message's eventual delivery
|
|
338
|
+
// (a user-role `message_start` event) leaves any draft the user has
|
|
339
|
+
// typed since queuing intact. Same protection as #783, applied to
|
|
340
|
+
// the streaming/queue path.
|
|
341
|
+
this.ctx.locallySubmittedUserSignatures.add(`${text}\u0000${images?.length ?? 0}`);
|
|
328
342
|
await this.ctx.session.prompt(text, { streamingBehavior: "steer", images });
|
|
329
343
|
this.ctx.updatePendingMessagesDisplay();
|
|
330
344
|
this.ctx.ui.requestRender();
|
|
@@ -434,6 +448,7 @@ export class InputController {
|
|
|
434
448
|
}
|
|
435
449
|
|
|
436
450
|
restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
|
|
451
|
+
this.ctx.locallySubmittedUserSignatures.clear();
|
|
437
452
|
const { steering, followUp } = this.ctx.session.clearQueue();
|
|
438
453
|
const allQueued = [...steering, ...followUp];
|
|
439
454
|
if (allQueued.length === 0) {
|
|
@@ -145,6 +145,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
145
145
|
planModeEnabled = false;
|
|
146
146
|
planModePaused = false;
|
|
147
147
|
planModePlanFilePath: string | undefined = undefined;
|
|
148
|
+
loopModeEnabled = false;
|
|
149
|
+
loopPrompt: string | undefined = undefined;
|
|
150
|
+
#loopAutoSubmitTimer: NodeJS.Timeout | undefined;
|
|
148
151
|
todoPhases: TodoPhase[] = [];
|
|
149
152
|
hideThinkingBlock = false;
|
|
150
153
|
pendingImages: ImageContent[] = [];
|
|
@@ -167,6 +170,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
167
170
|
unsubscribe?: () => void;
|
|
168
171
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
169
172
|
optimisticUserMessageSignature: string | undefined = undefined;
|
|
173
|
+
locallySubmittedUserSignatures: Set<string> = new Set();
|
|
170
174
|
#pendingSubmittedInput: SubmittedUserInput | undefined;
|
|
171
175
|
lastSigintTime = 0;
|
|
172
176
|
lastEscapeTime = 0;
|
|
@@ -483,9 +487,75 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
483
487
|
this.onInputCallback = undefined;
|
|
484
488
|
resolve(input);
|
|
485
489
|
};
|
|
490
|
+
this.#scheduleLoopAutoSubmit();
|
|
486
491
|
return promise;
|
|
487
492
|
}
|
|
488
493
|
|
|
494
|
+
#scheduleLoopAutoSubmit(): void {
|
|
495
|
+
if (this.#loopAutoSubmitTimer) {
|
|
496
|
+
clearTimeout(this.#loopAutoSubmitTimer);
|
|
497
|
+
this.#loopAutoSubmitTimer = undefined;
|
|
498
|
+
}
|
|
499
|
+
if (!this.loopModeEnabled || !this.loopPrompt) return;
|
|
500
|
+
const prompt = this.loopPrompt;
|
|
501
|
+
const loopAction = settings.get("loop.mode");
|
|
502
|
+
// Brief delay so the user has a chance to press Esc between iterations.
|
|
503
|
+
this.#loopAutoSubmitTimer = setTimeout(() => {
|
|
504
|
+
this.#loopAutoSubmitTimer = undefined;
|
|
505
|
+
if (!this.loopModeEnabled || !this.onInputCallback) return;
|
|
506
|
+
void this.#runLoopIteration(loopAction, prompt);
|
|
507
|
+
}, 800);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async #runLoopIteration(action: "prompt" | "compact" | "reset", prompt: string): Promise<void> {
|
|
511
|
+
if (action === "compact") {
|
|
512
|
+
await this.handleCompactCommand();
|
|
513
|
+
} else if (action === "reset") {
|
|
514
|
+
await this.handleClearCommand();
|
|
515
|
+
}
|
|
516
|
+
if (!this.loopModeEnabled || !this.onInputCallback) return;
|
|
517
|
+
this.onInputCallback(this.startPendingSubmission({ text: prompt }));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
disableLoopMode(options?: { silent?: boolean }): void {
|
|
521
|
+
const wasEnabled = this.loopModeEnabled;
|
|
522
|
+
this.loopModeEnabled = false;
|
|
523
|
+
this.loopPrompt = undefined;
|
|
524
|
+
if (this.#loopAutoSubmitTimer) {
|
|
525
|
+
clearTimeout(this.#loopAutoSubmitTimer);
|
|
526
|
+
this.#loopAutoSubmitTimer = undefined;
|
|
527
|
+
}
|
|
528
|
+
this.statusLine.setLoopModeStatus(undefined);
|
|
529
|
+
this.updateEditorTopBorder();
|
|
530
|
+
this.ui.requestRender();
|
|
531
|
+
if (wasEnabled && !options?.silent) {
|
|
532
|
+
this.showStatus("Loop mode disabled.");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async handleLoopCommand(prompt?: string): Promise<void> {
|
|
537
|
+
if (this.loopModeEnabled) {
|
|
538
|
+
this.disableLoopMode();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const trimmed = prompt?.trim();
|
|
542
|
+
if (!trimmed) {
|
|
543
|
+
this.showError("Usage: /loop <prompt>");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
this.loopModeEnabled = true;
|
|
547
|
+
this.loopPrompt = trimmed;
|
|
548
|
+
this.statusLine.setLoopModeStatus({ enabled: true });
|
|
549
|
+
this.updateEditorTopBorder();
|
|
550
|
+
this.ui.requestRender();
|
|
551
|
+
this.showStatus("Loop mode enabled. Esc to stop.");
|
|
552
|
+
|
|
553
|
+
// Submit the first iteration immediately so the loop kicks off.
|
|
554
|
+
if (this.onInputCallback) {
|
|
555
|
+
this.onInputCallback(this.startPendingSubmission({ text: trimmed }));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
489
559
|
startPendingSubmission(input: { text: string; images?: ImageContent[] }): SubmittedUserInput {
|
|
490
560
|
const submission: SubmittedUserInput = {
|
|
491
561
|
text: input.text,
|
|
@@ -495,6 +565,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
495
565
|
};
|
|
496
566
|
this.#pendingSubmittedInput = submission;
|
|
497
567
|
this.optimisticUserMessageSignature = `${submission.text}\u0000${submission.images?.length ?? 0}`;
|
|
568
|
+
this.locallySubmittedUserSignatures.add(this.optimisticUserMessageSignature);
|
|
498
569
|
this.addMessageToChat({
|
|
499
570
|
role: "user",
|
|
500
571
|
content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
|
|
@@ -516,6 +587,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
516
587
|
submission.cancelled = true;
|
|
517
588
|
this.#pendingSubmittedInput = undefined;
|
|
518
589
|
this.optimisticUserMessageSignature = undefined;
|
|
590
|
+
this.locallySubmittedUserSignatures.delete(`${submission.text}\u0000${submission.images?.length ?? 0}`);
|
|
519
591
|
this.#pendingWorkingMessage = undefined;
|
|
520
592
|
if (this.loadingAnimation) {
|
|
521
593
|
this.loadingAnimation.stop();
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -91,6 +91,7 @@ export type SymbolKey =
|
|
|
91
91
|
// Icons
|
|
92
92
|
| "icon.model"
|
|
93
93
|
| "icon.plan"
|
|
94
|
+
| "icon.loop"
|
|
94
95
|
| "icon.folder"
|
|
95
96
|
| "icon.file"
|
|
96
97
|
| "icon.git"
|
|
@@ -250,6 +251,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
250
251
|
// Icons
|
|
251
252
|
"icon.model": "⬢",
|
|
252
253
|
"icon.plan": "🗺",
|
|
254
|
+
"icon.loop": "↻",
|
|
253
255
|
"icon.folder": "📁",
|
|
254
256
|
"icon.file": "📄",
|
|
255
257
|
"icon.git": "⎇",
|
|
@@ -460,6 +462,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
460
462
|
"icon.model": "\uec19",
|
|
461
463
|
// pick: | alt:
|
|
462
464
|
"icon.plan": "\uf2d2",
|
|
465
|
+
// pick: ↻ | alt: ⟳
|
|
466
|
+
"icon.loop": "\uf021",
|
|
463
467
|
// pick: | alt:
|
|
464
468
|
"icon.folder": "\uf115",
|
|
465
469
|
// pick: | alt:
|
|
@@ -659,6 +663,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
659
663
|
// Icons
|
|
660
664
|
"icon.model": "[M]",
|
|
661
665
|
"icon.plan": "plan",
|
|
666
|
+
"icon.loop": "loop",
|
|
662
667
|
"icon.folder": "[D]",
|
|
663
668
|
"icon.file": "[F]",
|
|
664
669
|
"icon.git": "git:",
|
|
@@ -1434,6 +1439,7 @@ export class Theme {
|
|
|
1434
1439
|
return {
|
|
1435
1440
|
model: this.#symbols["icon.model"],
|
|
1436
1441
|
plan: this.#symbols["icon.plan"],
|
|
1442
|
+
loop: this.#symbols["icon.loop"],
|
|
1437
1443
|
folder: this.#symbols["icon.folder"],
|
|
1438
1444
|
file: this.#symbols["icon.file"],
|
|
1439
1445
|
git: this.#symbols["icon.git"],
|