@gajae-code/coding-agent 0.4.5 → 0.5.1
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 +62 -0
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +7 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +30 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +8 -1
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/harness-control-plane/types.d.ts +4 -0
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +3 -1
- package/dist/types/session/blob-store.d.ts +59 -4
- package/dist/types/session/session-manager.d.ts +24 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +7 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/tools/subagent.d.ts +6 -0
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli.ts +9 -4
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +43 -5
- package/src/commands/launch.ts +2 -2
- package/src/commands/session.ts +3 -1
- package/src/config/file-lock-gc.ts +181 -0
- package/src/config/file-lock.ts +14 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +264 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +513 -26
- package/src/cursor.ts +16 -2
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/export/html/index.ts +13 -9
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +58 -7
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +46 -29
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +211 -8
- package/src/gjc-runtime/tmux-common.ts +29 -0
- package/src/gjc-runtime/tmux-gc.ts +176 -0
- package/src/gjc-runtime/tmux-sessions.ts +68 -12
- package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +89 -27
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +93 -0
- package/src/harness-control-plane/types.ts +4 -0
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +14 -8
- package/src/main.ts +7 -2
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +370 -181
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +34 -42
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +187 -39
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/sdk.ts +46 -5
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +179 -25
- package/src/session/blob-store.ts +148 -6
- package/src/session/session-manager.ts +311 -60
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +78 -6
- package/src/task/receipt.ts +5 -0
- package/src/task/render.ts +21 -1
- package/src/task/types.ts +8 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/ask.ts +56 -1
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +9 -0
- package/src/tools/subagent.ts +26 -2
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/tool-choice.ts +45 -16
package/src/main.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { activateModelProfile } from "./config/model-profile-activation";
|
|
|
30
30
|
import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
|
|
31
31
|
import { resolveCliModel, resolveModelRoleValue, resolveModelScope, type ScopedModel } from "./config/model-resolver";
|
|
32
32
|
import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
|
|
33
|
+
import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory } from "./defaults/gjc-grok-cli";
|
|
33
34
|
import { initializeWithSettings } from "./discovery";
|
|
34
35
|
import { exportFromFile } from "./export/html";
|
|
35
36
|
import type { ExtensionUIContext } from "./extensibility/extensions/types";
|
|
@@ -742,7 +743,9 @@ export async function runRootCommand(
|
|
|
742
743
|
await runListModelsCommand({
|
|
743
744
|
modelRegistry,
|
|
744
745
|
cwd: getProjectDir(),
|
|
745
|
-
|
|
746
|
+
extensionFactories: [
|
|
747
|
+
{ factory: getBundledGrokBuildExtensionFactory(), name: BUNDLED_GROK_BUILD_EXTENSION_ID },
|
|
748
|
+
],
|
|
746
749
|
settingsExtensions: [],
|
|
747
750
|
disabledExtensionIds: [],
|
|
748
751
|
disableExtensionDiscovery: true,
|
|
@@ -975,7 +978,9 @@ export async function runRootCommand(
|
|
|
975
978
|
|
|
976
979
|
if (mode === "rpc" || mode === "rpc-ui") {
|
|
977
980
|
const { runRpcMode } = await import("./modes/rpc/rpc-mode");
|
|
978
|
-
await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined
|
|
981
|
+
await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, {
|
|
982
|
+
listen: parsedArgs.rpcListen,
|
|
983
|
+
});
|
|
979
984
|
} else if (mode === "bridge") {
|
|
980
985
|
const { runBridgeMode } = await import("./modes/bridge/bridge-mode");
|
|
981
986
|
await runBridgeMode(session, setToolUIContext);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AssistantMessage, ImageContent, Usage } from "@gajae-code/ai";
|
|
2
|
-
import { Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@gajae-code/tui";
|
|
2
|
+
import { type Component, Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@gajae-code/tui";
|
|
3
3
|
import { formatNumber } from "@gajae-code/utils";
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
5
|
import { renderDeepInterviewAssistantText } from "../../deep-interview/render-middleware";
|
|
@@ -18,6 +18,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
18
18
|
#convertedKittyImages = new Map<string, ImageContent>();
|
|
19
19
|
#kittyConversionsInFlight = new Set<string>();
|
|
20
20
|
#responseHeader = new Text(theme.bold(theme.fg("statusLineModel", "gajae")), 1, 0);
|
|
21
|
+
#contentBlocksCache = new WeakMap<object, { source: string; component: Component }>();
|
|
21
22
|
|
|
22
23
|
constructor(
|
|
23
24
|
message?: AssistantMessage,
|
|
@@ -106,6 +107,28 @@ export class AssistantMessageComponent extends Container {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
#renderTextBlock(content: { text: string }): Component {
|
|
111
|
+
const cached = this.#contentBlocksCache.get(content);
|
|
112
|
+
if (cached?.source === content.text) return cached.component;
|
|
113
|
+
const trimmed = content.text.trim();
|
|
114
|
+
const component =
|
|
115
|
+
renderDeepInterviewAssistantText(trimmed, theme) ?? new Markdown(trimmed, 1, 0, getMarkdownTheme());
|
|
116
|
+
this.#contentBlocksCache.set(content, { source: content.text, component });
|
|
117
|
+
return component;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#renderThinkingBlock(content: { thinking: string }): Markdown {
|
|
121
|
+
const cached = this.#contentBlocksCache.get(content);
|
|
122
|
+
if (cached?.source === content.thinking) return cached.component as Markdown;
|
|
123
|
+
const trimmed = content.thinking.trim();
|
|
124
|
+
const component = new Markdown(trimmed, 1, 0, getMarkdownTheme(), {
|
|
125
|
+
color: (text: string) => theme.fg("thinkingText", text),
|
|
126
|
+
italic: true,
|
|
127
|
+
});
|
|
128
|
+
this.#contentBlocksCache.set(content, { source: content.thinking, component });
|
|
129
|
+
return component;
|
|
130
|
+
}
|
|
131
|
+
|
|
109
132
|
#renderToolImages(): void {
|
|
110
133
|
const imageEntries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
|
|
111
134
|
images.map((image, index) => ({ image, key: `${toolCallId}:${index}` })),
|
|
@@ -152,12 +175,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
152
175
|
for (let i = 0; i < message.content.length; i++) {
|
|
153
176
|
const content = message.content[i];
|
|
154
177
|
if (content.type === "text" && content.text.trim()) {
|
|
155
|
-
|
|
156
|
-
// Set paddingY=0 to avoid extra spacing before tool executions
|
|
157
|
-
const text = content.text.trim();
|
|
158
|
-
this.#contentContainer.addChild(
|
|
159
|
-
renderDeepInterviewAssistantText(text, theme) ?? new Markdown(text, 1, 0, getMarkdownTheme()),
|
|
160
|
-
);
|
|
178
|
+
this.#contentContainer.addChild(this.#renderTextBlock(content));
|
|
161
179
|
} else if (content.type === "thinking" && content.thinking.trim()) {
|
|
162
180
|
// Add spacing only when another visible assistant content block follows.
|
|
163
181
|
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
|
@@ -172,13 +190,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
172
190
|
this.#contentContainer.addChild(new Spacer(1));
|
|
173
191
|
}
|
|
174
192
|
} else {
|
|
175
|
-
|
|
176
|
-
this.#contentContainer.addChild(
|
|
177
|
-
new Markdown(content.thinking.trim(), 1, 0, getMarkdownTheme(), {
|
|
178
|
-
color: (text: string) => theme.fg("thinkingText", text),
|
|
179
|
-
italic: true,
|
|
180
|
-
}),
|
|
181
|
-
);
|
|
193
|
+
this.#contentContainer.addChild(this.#renderThinkingBlock(content));
|
|
182
194
|
if (hasVisibleContentAfter) {
|
|
183
195
|
this.#contentContainer.addChild(new Spacer(1));
|
|
184
196
|
}
|
|
@@ -6,6 +6,9 @@ import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs } from "../../to
|
|
|
6
6
|
/** SGR dim on / normal intensity — additive, preserves fg/bg colors. */
|
|
7
7
|
const DIM = "\x1b[2m";
|
|
8
8
|
const DIM_OFF = "\x1b[22m";
|
|
9
|
+
// Single-span fast path is tuned for rendered code lines; ~500 chars covers typical terminal widths
|
|
10
|
+
// while avoiding duplicate prefix/suffix scans before diffWords on pathological long lines.
|
|
11
|
+
const LONG_LINE_FAST_PATH_LIMIT = 500;
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Visualize leading whitespace (indentation) with dim glyphs.
|
|
@@ -53,6 +56,15 @@ function parseDiffLine(line: string): { prefix: CodeFrameMarker; lineNum: string
|
|
|
53
56
|
* Strips leading whitespace from inverse to avoid highlighting indentation.
|
|
54
57
|
*/
|
|
55
58
|
function renderIntraLineDiff(oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
|
|
59
|
+
const fastPath = renderIntraLineDiffFastPath(oldContent, newContent);
|
|
60
|
+
if (fastPath) return fastPath;
|
|
61
|
+
return renderIntraLineDiffWithDiffWords(oldContent, newContent);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderIntraLineDiffWithDiffWords(
|
|
65
|
+
oldContent: string,
|
|
66
|
+
newContent: string,
|
|
67
|
+
): { removedLine: string; addedLine: string } {
|
|
56
68
|
const wordDiff = Diff.diffWords(oldContent, newContent);
|
|
57
69
|
|
|
58
70
|
let removedLine = "";
|
|
@@ -94,6 +106,91 @@ function renderIntraLineDiff(oldContent: string, newContent: string): { removedL
|
|
|
94
106
|
return { removedLine, addedLine };
|
|
95
107
|
}
|
|
96
108
|
|
|
109
|
+
function renderIntraLineDiffFastPath(
|
|
110
|
+
oldContent: string,
|
|
111
|
+
newContent: string,
|
|
112
|
+
): { removedLine: string; addedLine: string } | null {
|
|
113
|
+
if (oldContent === newContent) return { removedLine: oldContent, addedLine: newContent };
|
|
114
|
+
if (Math.min(oldContent.length, newContent.length) > LONG_LINE_FAST_PATH_LIMIT) return null;
|
|
115
|
+
|
|
116
|
+
if (isWhitespaceOnlyChange(oldContent, newContent)) return null;
|
|
117
|
+
return renderSingleSpanIntraLineDiff(oldContent, newContent);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isWhitespaceOnlyChange(oldContent: string, newContent: string): boolean {
|
|
121
|
+
return oldContent.replace(/\s+/g, "") === newContent.replace(/\s+/g, "");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderSingleSpanIntraLineDiff(
|
|
125
|
+
oldContent: string,
|
|
126
|
+
newContent: string,
|
|
127
|
+
): { removedLine: string; addedLine: string } | null {
|
|
128
|
+
let prefixLength = 0;
|
|
129
|
+
const maxPrefixLength = Math.min(oldContent.length, newContent.length);
|
|
130
|
+
while (
|
|
131
|
+
prefixLength < maxPrefixLength &&
|
|
132
|
+
oldContent.charCodeAt(prefixLength) === newContent.charCodeAt(prefixLength)
|
|
133
|
+
) {
|
|
134
|
+
prefixLength++;
|
|
135
|
+
if (prefixLength > LONG_LINE_FAST_PATH_LIMIT) return null;
|
|
136
|
+
}
|
|
137
|
+
let suffixLength = 0;
|
|
138
|
+
const maxSuffixLength = maxPrefixLength - prefixLength;
|
|
139
|
+
while (
|
|
140
|
+
suffixLength < maxSuffixLength &&
|
|
141
|
+
oldContent.charCodeAt(oldContent.length - 1 - suffixLength) ===
|
|
142
|
+
newContent.charCodeAt(newContent.length - 1 - suffixLength)
|
|
143
|
+
) {
|
|
144
|
+
suffixLength++;
|
|
145
|
+
if (prefixLength + suffixLength > LONG_LINE_FAST_PATH_LIMIT) return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const oldMiddle = oldContent.slice(prefixLength, oldContent.length - suffixLength);
|
|
149
|
+
const newMiddle = newContent.slice(prefixLength, newContent.length - suffixLength);
|
|
150
|
+
if (oldMiddle.length === 0 || newMiddle.length === 0) return null;
|
|
151
|
+
if (!isSingleDiffWordsReplacement(oldContent, newContent, prefixLength, suffixLength)) return null;
|
|
152
|
+
|
|
153
|
+
const prefix = oldContent.slice(0, prefixLength);
|
|
154
|
+
const oldLeadingWs = oldMiddle.match(/^(\s*)/)?.[1] || "";
|
|
155
|
+
const newLeadingWs = newMiddle.match(/^(\s*)/)?.[1] || "";
|
|
156
|
+
return {
|
|
157
|
+
removedLine: `${prefix}${oldLeadingWs}${theme.inverse(oldMiddle.slice(oldLeadingWs.length))}${oldContent.slice(oldContent.length - suffixLength)}`,
|
|
158
|
+
addedLine: `${prefix}${newLeadingWs}${theme.inverse(newMiddle.slice(newLeadingWs.length))}${newContent.slice(newContent.length - suffixLength)}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isSingleDiffWordsReplacement(
|
|
163
|
+
oldContent: string,
|
|
164
|
+
newContent: string,
|
|
165
|
+
prefixLength: number,
|
|
166
|
+
suffixLength: number,
|
|
167
|
+
): boolean {
|
|
168
|
+
if (prefixLength === 0 && suffixLength === 0) return false;
|
|
169
|
+
const oldEnd = oldContent.length - suffixLength;
|
|
170
|
+
const newEnd = newContent.length - suffixLength;
|
|
171
|
+
const snappedPrefixLength = snapPrefixToWhitespaceBoundary(oldContent, newContent, prefixLength);
|
|
172
|
+
const snappedOldEnd = snapEndToWhitespaceBoundary(oldContent, oldEnd);
|
|
173
|
+
const snappedNewEnd = snapEndToWhitespaceBoundary(newContent, newEnd);
|
|
174
|
+
return snappedPrefixLength === prefixLength && snappedOldEnd === oldEnd && snappedNewEnd === newEnd;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function snapPrefixToWhitespaceBoundary(oldContent: string, newContent: string, prefixLength: number): number {
|
|
178
|
+
let snapped = prefixLength;
|
|
179
|
+
while (snapped > 0 && !(isWhitespaceBoundary(oldContent, snapped) && isWhitespaceBoundary(newContent, snapped)))
|
|
180
|
+
snapped--;
|
|
181
|
+
return snapped;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function snapEndToWhitespaceBoundary(content: string, end: number): number {
|
|
185
|
+
let snapped = end;
|
|
186
|
+
while (snapped < content.length && !isWhitespaceBoundary(content, snapped)) snapped++;
|
|
187
|
+
return snapped;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isWhitespaceBoundary(content: string, index: number): boolean {
|
|
191
|
+
return index <= 0 || index >= content.length || /\s/.test(content[index - 1]!) || /\s/.test(content[index]!);
|
|
192
|
+
}
|
|
193
|
+
|
|
97
194
|
export interface RenderDiffOptions {
|
|
98
195
|
/** File path used to resolve indentation (.editorconfig + defaults) */
|
|
99
196
|
filePath?: string;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Displays a list of string options with keyboard navigation.
|
|
4
4
|
*/
|
|
5
5
|
import {
|
|
6
|
+
type AutocompleteProvider,
|
|
6
7
|
Container,
|
|
7
8
|
Editor,
|
|
8
9
|
Markdown,
|
|
@@ -73,6 +74,12 @@ export interface HookSelectorOptions {
|
|
|
73
74
|
optionLabel: string;
|
|
74
75
|
onSubmit: (text: string) => void;
|
|
75
76
|
};
|
|
77
|
+
/**
|
|
78
|
+
* Autocomplete provider for the inline custom-input editor. When present,
|
|
79
|
+
* the "Other (type your own)" editor gains the same `@` file-link and `/`
|
|
80
|
+
* completion behavior as the main prompt editor.
|
|
81
|
+
*/
|
|
82
|
+
autocompleteProvider?: AutocompleteProvider;
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
class OutlinedList extends Container {
|
|
@@ -320,6 +327,7 @@ export class HookSelectorComponent extends Container {
|
|
|
320
327
|
#helpTextComponent: Text;
|
|
321
328
|
#baseHelpText: string;
|
|
322
329
|
#tui: TUI | undefined;
|
|
330
|
+
#autocompleteProvider: AutocompleteProvider | undefined;
|
|
323
331
|
constructor(
|
|
324
332
|
title: string,
|
|
325
333
|
options: string[],
|
|
@@ -342,6 +350,7 @@ export class HookSelectorComponent extends Container {
|
|
|
342
350
|
this.#outline = opts?.outline === true;
|
|
343
351
|
this.#customInput = opts?.customInput;
|
|
344
352
|
this.#tui = opts?.tui;
|
|
353
|
+
this.#autocompleteProvider = opts?.autocompleteProvider;
|
|
345
354
|
|
|
346
355
|
this.addChild(new DynamicBorder());
|
|
347
356
|
this.addChild(new Spacer(1));
|
|
@@ -491,6 +500,13 @@ export class HookSelectorComponent extends Container {
|
|
|
491
500
|
|
|
492
501
|
/** Keys while the inline custom-input editor is open below the option list. */
|
|
493
502
|
#handleInputModeKey(keyData: string, editor: Editor): void {
|
|
503
|
+
// While the autocomplete dropdown is open, every key belongs to the
|
|
504
|
+
// editor (navigate/apply/cancel the suggestion) instead of submitting
|
|
505
|
+
// or backing out of input mode.
|
|
506
|
+
if (editor.isAutocompleteOpen()) {
|
|
507
|
+
editor.handleInput(keyData);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
494
510
|
// Escape backs out to option selection instead of cancelling the dialog,
|
|
495
511
|
// so a stray Esc never throws away the question context.
|
|
496
512
|
if (matchesKey(keyData, "escape") || matchesAppInterrupt(keyData)) {
|
|
@@ -521,6 +537,9 @@ export class HookSelectorComponent extends Container {
|
|
|
521
537
|
editor.setBorderVisible(false);
|
|
522
538
|
editor.setPromptGutter("> ");
|
|
523
539
|
editor.disableSubmit = true;
|
|
540
|
+
if (this.#autocompleteProvider) {
|
|
541
|
+
editor.setAutocompleteProvider(this.#autocompleteProvider);
|
|
542
|
+
}
|
|
524
543
|
this.#inlineEditor = editor;
|
|
525
544
|
this.#inputArea.addChild(new Spacer(1));
|
|
526
545
|
this.#inputArea.addChild(editor);
|