@gajae-code/coding-agent 0.15.4 → 0.15.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -2
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +27 -5
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +99 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -4
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +19 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -5
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +142 -6
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +33 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1028 -123
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +14 -1
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
- package/src/sdk/bus/prompt-reconciliation.ts +33 -6
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +161 -5
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +1240 -107
- package/src/sdk/host/session-runtime.ts +1029 -115
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +297 -111
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +887 -95
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +29 -10
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +786 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
|
|
4
|
-
import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@gajae-code/tui";
|
|
4
|
+
import { type AutocompleteProvider, getKeybindings, matchesKey, type SlashCommand } from "@gajae-code/tui";
|
|
5
5
|
import { $pickenv, logger, sanitizeText } from "@gajae-code/utils";
|
|
6
6
|
import { type AppKeybinding, KEYBINDINGS } from "../../config/keybindings";
|
|
7
7
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
@@ -1855,19 +1855,36 @@ export class InputController {
|
|
|
1855
1855
|
const now = Date.now();
|
|
1856
1856
|
if (now - this.#lastBackgroundFoldKeyTime > BACKGROUND_FOLD_DOUBLE_PRESS_MS) {
|
|
1857
1857
|
this.#lastBackgroundFoldKeyTime = now;
|
|
1858
|
-
|
|
1858
|
+
const foldKeys = getKeybindings().getKeys("app.tool.backgroundFold");
|
|
1859
|
+
this.ctx.showStatus(
|
|
1860
|
+
`Press ${foldKeys.join(" / ")} again to fold supported foreground bash into a background job`,
|
|
1861
|
+
);
|
|
1859
1862
|
return true;
|
|
1860
1863
|
}
|
|
1861
1864
|
this.#lastBackgroundFoldKeyTime = 0;
|
|
1862
1865
|
|
|
1863
|
-
|
|
1866
|
+
try {
|
|
1867
|
+
const request = this.ctx.session.requestForegroundBashBackground?.() ?? Promise.resolve(false);
|
|
1868
|
+
void request
|
|
1869
|
+
.then(backgrounded => {
|
|
1870
|
+
if (backgrounded) {
|
|
1871
|
+
this.ctx.showStatus("Folding foreground bash into a quiet background job…");
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
this.ctx.showWarning(
|
|
1875
|
+
"No supported foreground tool can be folded. Use managed async bash/auto-background; raw Ctrl+Z/bg is not supported inside the TUI.",
|
|
1876
|
+
);
|
|
1877
|
+
})
|
|
1878
|
+
.catch(() => {
|
|
1879
|
+
this.ctx.showWarning(
|
|
1880
|
+
"No supported foreground tool can be folded. Use managed async bash/auto-background; raw Ctrl+Z/bg is not supported inside the TUI.",
|
|
1881
|
+
);
|
|
1882
|
+
});
|
|
1883
|
+
} catch {
|
|
1864
1884
|
this.ctx.showWarning(
|
|
1865
1885
|
"No supported foreground tool can be folded. Use managed async bash/auto-background; raw Ctrl+Z/bg is not supported inside the TUI.",
|
|
1866
1886
|
);
|
|
1867
|
-
return true;
|
|
1868
1887
|
}
|
|
1869
|
-
|
|
1870
|
-
this.ctx.showStatus("Folding foreground bash into a quiet background job…");
|
|
1871
1888
|
return true;
|
|
1872
1889
|
}
|
|
1873
1890
|
|
|
@@ -426,6 +426,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
426
426
|
lastComposerClearEscapeTime = 0;
|
|
427
427
|
shutdownRequested = false;
|
|
428
428
|
#isShuttingDown = false;
|
|
429
|
+
#shutdownEscalated = false;
|
|
429
430
|
hookSelector: HookSelectorComponent | undefined = undefined;
|
|
430
431
|
hookInput: HookInputComponent | undefined = undefined;
|
|
431
432
|
hookEditor: HookEditorComponent | undefined = undefined;
|
|
@@ -1682,7 +1683,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1682
1683
|
}
|
|
1683
1684
|
|
|
1684
1685
|
async shutdown(): Promise<void> {
|
|
1685
|
-
if (this.#isShuttingDown)
|
|
1686
|
+
if (this.#isShuttingDown) {
|
|
1687
|
+
// A second explicit exit is an escalation, not another silent no-op. The
|
|
1688
|
+
// graceful path may be waiting on a lock-hostile filesystem; postmortem.quit
|
|
1689
|
+
// has its own bounded cleanup deadline and will force the process out even
|
|
1690
|
+
// when the first shutdown cannot finish.
|
|
1691
|
+
if (!this.#shutdownEscalated) {
|
|
1692
|
+
this.#shutdownEscalated = true;
|
|
1693
|
+
logger.warn("Escalating repeated interactive shutdown request");
|
|
1694
|
+
this.session.abort();
|
|
1695
|
+
void postmortem.quit(1);
|
|
1696
|
+
}
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1686
1699
|
this.#isShuttingDown = true;
|
|
1687
1700
|
|
|
1688
1701
|
// `/btw` owns the shared composer while its panel is open. Never persist a
|
|
@@ -16,12 +16,48 @@ import type { AsyncJob, AsyncJobManager } from "../async";
|
|
|
16
16
|
import { deleteCronJobById, listCronSnapshots, onCronChange } from "../tools/cron";
|
|
17
17
|
|
|
18
18
|
export type JobsWorstState = "none" | "running" | "failed";
|
|
19
|
+
export type JobDeliveryState = "pending" | "delivered" | "failed-visible";
|
|
20
|
+
|
|
21
|
+
/** The manager-owned, one-pass job projection consumed by this observer. */
|
|
22
|
+
export interface AsyncJobSnapshotEntry {
|
|
23
|
+
id: string;
|
|
24
|
+
kind: string;
|
|
25
|
+
label: string;
|
|
26
|
+
status: AsyncJob["status"];
|
|
27
|
+
generation: string;
|
|
28
|
+
backgrounded: boolean;
|
|
29
|
+
deliveryState: JobDeliveryState;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Scalar delivery failure evidence that may outlive the AsyncJob record. */
|
|
33
|
+
export interface DeadLetteredJobSnapshotEntry {
|
|
34
|
+
jobId: string;
|
|
35
|
+
generation: string;
|
|
36
|
+
ownerId?: string;
|
|
37
|
+
backgrounded?: boolean;
|
|
38
|
+
attempt: number;
|
|
39
|
+
lastError?: string;
|
|
40
|
+
recordedAt: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AsyncJobsSnapshot {
|
|
44
|
+
jobs: AsyncJobSnapshotEntry[];
|
|
45
|
+
deadLettered: DeadLetteredJobSnapshotEntry[];
|
|
46
|
+
}
|
|
19
47
|
|
|
20
48
|
export interface MonitorJobView {
|
|
21
49
|
id: string;
|
|
22
50
|
label: string;
|
|
23
51
|
status: AsyncJob["status"];
|
|
24
52
|
startTime: number;
|
|
53
|
+
generation?: string;
|
|
54
|
+
backgrounded?: boolean;
|
|
55
|
+
deliveryState?: JobDeliveryState;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FoldedJobView extends AsyncJobSnapshotEntry {
|
|
59
|
+
/** Safe diagnostic retained for a dead-lettered delivery, when available. */
|
|
60
|
+
errorText?: string;
|
|
25
61
|
}
|
|
26
62
|
|
|
27
63
|
export interface CronJobView {
|
|
@@ -39,6 +75,10 @@ export interface CronJobView {
|
|
|
39
75
|
export interface JobsSnapshot {
|
|
40
76
|
monitors: MonitorJobView[];
|
|
41
77
|
crons: CronJobView[];
|
|
78
|
+
/** Backgrounded work and undeliverable terminal work that must stay visible. */
|
|
79
|
+
foldedJobs?: FoldedJobView[];
|
|
80
|
+
/** Scalar dead-letter evidence retained for diagnostics and red-drill-in. */
|
|
81
|
+
deadLettered?: DeadLetteredJobSnapshotEntry[];
|
|
42
82
|
activeMonitorCount: number;
|
|
43
83
|
activeCronCount: number;
|
|
44
84
|
worstState: JobsWorstState;
|
|
@@ -48,12 +88,22 @@ export interface JobsSnapshot {
|
|
|
48
88
|
export const EMPTY_JOBS_SNAPSHOT: JobsSnapshot = {
|
|
49
89
|
monitors: [],
|
|
50
90
|
crons: [],
|
|
91
|
+
foldedJobs: [],
|
|
92
|
+
deadLettered: [],
|
|
51
93
|
activeMonitorCount: 0,
|
|
52
94
|
activeCronCount: 0,
|
|
53
95
|
worstState: "none",
|
|
54
96
|
failedUnacknowledged: false,
|
|
55
97
|
};
|
|
56
98
|
|
|
99
|
+
function jobKey(id: string, generation: string): string {
|
|
100
|
+
return `${id}\u0000${generation}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface AsyncJobSnapshotSource {
|
|
104
|
+
getJobsSnapshot?: (filter?: { ownerId?: string }) => AsyncJobsSnapshot;
|
|
105
|
+
}
|
|
106
|
+
|
|
57
107
|
export class JobsObserver {
|
|
58
108
|
readonly #manager: AsyncJobManager;
|
|
59
109
|
readonly #ownerId: string | undefined;
|
|
@@ -103,7 +153,14 @@ export class JobsObserver {
|
|
|
103
153
|
}
|
|
104
154
|
}
|
|
105
155
|
|
|
156
|
+
#readAsyncJobsSnapshot(): AsyncJobsSnapshot {
|
|
157
|
+
const source = this.#manager as AsyncJobSnapshotSource;
|
|
158
|
+
if (typeof source.getJobsSnapshot !== "function") return { jobs: [], deadLettered: [] };
|
|
159
|
+
return source.getJobsSnapshot(this.#ownerId ? { ownerId: this.#ownerId } : undefined);
|
|
160
|
+
}
|
|
161
|
+
|
|
106
162
|
#listMonitorJobs(): AsyncJob[] {
|
|
163
|
+
if (typeof this.#manager.getAllJobs !== "function") return [];
|
|
107
164
|
const filter = this.#ownerId ? { ownerId: this.#ownerId } : undefined;
|
|
108
165
|
return this.#manager.getAllJobs(filter).filter(job => job.type === "bash" && job.metadata?.monitor === true);
|
|
109
166
|
}
|
|
@@ -114,25 +171,92 @@ export class JobsObserver {
|
|
|
114
171
|
* snapshot (never scans the manager/cron stores).
|
|
115
172
|
*/
|
|
116
173
|
#recompute(): void {
|
|
174
|
+
const asyncSnapshot = this.#readAsyncJobsSnapshot();
|
|
175
|
+
const snapshotEntries = new Map<string, AsyncJobSnapshotEntry>();
|
|
176
|
+
for (const entry of asyncSnapshot.jobs) snapshotEntries.set(jobKey(entry.id, entry.generation), entry);
|
|
177
|
+
|
|
117
178
|
const monitorJobs = this.#listMonitorJobs();
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
179
|
+
const monitorKeys = new Set(monitorJobs.map(job => jobKey(job.id, job.generation)));
|
|
180
|
+
const presentKeys = new Set<string>([
|
|
181
|
+
...asyncSnapshot.jobs.map(entry => jobKey(entry.id, entry.generation)),
|
|
182
|
+
...asyncSnapshot.deadLettered.map(entry => jobKey(entry.jobId, entry.generation)),
|
|
183
|
+
]);
|
|
184
|
+
// Prune acknowledged keys whose jobs and scalar dead letters have been evicted.
|
|
185
|
+
for (const key of this.#acknowledgedFailedIds) {
|
|
186
|
+
if (!presentKeys.has(key)) this.#acknowledgedFailedIds.delete(key);
|
|
122
187
|
}
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
const hasUnacknowledgedFailure =
|
|
127
|
-
|
|
128
|
-
|
|
188
|
+
|
|
189
|
+
// Sticky failure latch: the manager's terminal status and delivery
|
|
190
|
+
// classification are authoritative; neither is reconstructed from getters.
|
|
191
|
+
const hasUnacknowledgedFailure =
|
|
192
|
+
asyncSnapshot.jobs.some(
|
|
193
|
+
entry =>
|
|
194
|
+
(entry.deliveryState === "failed-visible" ||
|
|
195
|
+
(entry.status === "failed" &&
|
|
196
|
+
(entry.backgrounded || monitorKeys.has(jobKey(entry.id, entry.generation))))) &&
|
|
197
|
+
!this.#acknowledgedFailedIds.has(jobKey(entry.id, entry.generation)),
|
|
198
|
+
) ||
|
|
199
|
+
asyncSnapshot.deadLettered.some(
|
|
200
|
+
entry => !this.#acknowledgedFailedIds.has(jobKey(entry.jobId, entry.generation)),
|
|
201
|
+
);
|
|
129
202
|
if (hasUnacknowledgedFailure) this.#failedUnacknowledged = true;
|
|
130
203
|
|
|
131
|
-
const activeMonitors = monitorJobs.filter(job => job.status === "running");
|
|
132
|
-
const cronSnapshots = listCronSnapshots(this.#ownerId);
|
|
133
204
|
const monitors: MonitorJobView[] = monitorJobs
|
|
134
|
-
.
|
|
205
|
+
.flatMap(job => {
|
|
206
|
+
const entry = snapshotEntries.get(jobKey(job.id, job.generation));
|
|
207
|
+
if (!entry) return [];
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
id: entry.id,
|
|
211
|
+
label: entry.label,
|
|
212
|
+
status: entry.status,
|
|
213
|
+
startTime: job.startTime,
|
|
214
|
+
generation: entry.generation,
|
|
215
|
+
backgrounded: entry.backgrounded,
|
|
216
|
+
deliveryState: entry.deliveryState,
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
})
|
|
135
220
|
.sort((a, b) => b.startTime - a.startTime);
|
|
221
|
+
|
|
222
|
+
// Keep every backgrounded job visible. A non-delivered terminal entry is
|
|
223
|
+
// also retained here so pending/failed delivery can never disappear merely
|
|
224
|
+
// because it was not marked backgrounded by its producer.
|
|
225
|
+
const deadLettersByKey = new Map(
|
|
226
|
+
asyncSnapshot.deadLettered.map(entry => [jobKey(entry.jobId, entry.generation), entry] as const),
|
|
227
|
+
);
|
|
228
|
+
const foldedJobs: FoldedJobView[] = asyncSnapshot.jobs
|
|
229
|
+
.filter(
|
|
230
|
+
entry =>
|
|
231
|
+
entry.backgrounded ||
|
|
232
|
+
(entry.status !== "running" &&
|
|
233
|
+
(entry.deliveryState === "failed-visible" ||
|
|
234
|
+
(entry.status === "failed" && entry.backgrounded) ||
|
|
235
|
+
entry.deliveryState !== "delivered")),
|
|
236
|
+
)
|
|
237
|
+
.map(entry => {
|
|
238
|
+
const lastError = deadLettersByKey.get(jobKey(entry.id, entry.generation))?.lastError;
|
|
239
|
+
return lastError === undefined ? { ...entry } : { ...entry, errorText: lastError };
|
|
240
|
+
});
|
|
241
|
+
const foldedKeys = new Set(foldedJobs.map(entry => jobKey(entry.id, entry.generation)));
|
|
242
|
+
for (const entry of asyncSnapshot.deadLettered) {
|
|
243
|
+
const key = jobKey(entry.jobId, entry.generation);
|
|
244
|
+
if (foldedKeys.has(key)) continue;
|
|
245
|
+
foldedKeys.add(key);
|
|
246
|
+
foldedJobs.push({
|
|
247
|
+
id: entry.jobId,
|
|
248
|
+
kind: "dead-letter",
|
|
249
|
+
label: entry.jobId,
|
|
250
|
+
status: "failed",
|
|
251
|
+
generation: entry.generation,
|
|
252
|
+
backgrounded: entry.backgrounded === true,
|
|
253
|
+
deliveryState: "failed-visible",
|
|
254
|
+
errorText: entry.lastError,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const activeMonitors = monitors.filter(monitor => monitor.status === "running" && !monitor.backgrounded);
|
|
259
|
+
const cronSnapshots = listCronSnapshots(this.#ownerId);
|
|
136
260
|
const crons: CronJobView[] = cronSnapshots
|
|
137
261
|
.map(snapshot => ({
|
|
138
262
|
id: snapshot.id,
|
|
@@ -145,14 +269,17 @@ export class JobsObserver {
|
|
|
145
269
|
firing: snapshot.firing,
|
|
146
270
|
}))
|
|
147
271
|
.sort((a, b) => b.createdAt - a.createdAt);
|
|
272
|
+
const hasRunningFoldedJob = foldedJobs.some(job => job.status === "running");
|
|
148
273
|
const worstState: JobsWorstState = this.#failedUnacknowledged
|
|
149
274
|
? "failed"
|
|
150
|
-
: activeMonitors.length > 0 || crons.length > 0
|
|
275
|
+
: activeMonitors.length > 0 || crons.length > 0 || hasRunningFoldedJob
|
|
151
276
|
? "running"
|
|
152
277
|
: "none";
|
|
153
278
|
this.#snapshot = {
|
|
154
279
|
monitors,
|
|
155
280
|
crons,
|
|
281
|
+
foldedJobs,
|
|
282
|
+
deadLettered: asyncSnapshot.deadLettered.map(entry => ({ ...entry })),
|
|
156
283
|
activeMonitorCount: activeMonitors.length,
|
|
157
284
|
activeCronCount: crons.length,
|
|
158
285
|
worstState,
|
|
@@ -167,8 +294,14 @@ export class JobsObserver {
|
|
|
167
294
|
|
|
168
295
|
/** Clear the failure latch (called when the user opens the jobs overlay). */
|
|
169
296
|
acknowledgeFailures(): void {
|
|
170
|
-
|
|
171
|
-
|
|
297
|
+
const asyncSnapshot = this.#readAsyncJobsSnapshot();
|
|
298
|
+
for (const entry of asyncSnapshot.jobs) {
|
|
299
|
+
if (entry.status === "failed" || entry.deliveryState === "failed-visible") {
|
|
300
|
+
this.#acknowledgedFailedIds.add(jobKey(entry.id, entry.generation));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
for (const entry of asyncSnapshot.deadLettered) {
|
|
304
|
+
this.#acknowledgedFailedIds.add(jobKey(entry.jobId, entry.generation));
|
|
172
305
|
}
|
|
173
306
|
if (!this.#failedUnacknowledged) return;
|
|
174
307
|
this.#failedUnacknowledged = false;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AutocompleteItem,
|
|
3
3
|
type AutocompleteProvider,
|
|
4
|
+
autocompleteFuzzyMatch,
|
|
5
|
+
autocompleteFuzzyScore,
|
|
4
6
|
CombinedAutocompleteProvider,
|
|
5
7
|
extractSlashCommandTokenPrefix,
|
|
6
8
|
getSlashCommandMatchRank,
|
|
9
|
+
normalizeFuzzyText,
|
|
7
10
|
type SlashCommand,
|
|
8
11
|
} from "@gajae-code/tui";
|
|
9
12
|
import type { KeybindingsManager } from "../config/keybindings";
|
|
@@ -50,43 +53,6 @@ interface PromptActionAutocompleteOptions {
|
|
|
50
53
|
getPromptSuggestion?: () => string | null;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
function fuzzyMatch(query: string, target: string): boolean {
|
|
54
|
-
if (query.length === 0) return true;
|
|
55
|
-
if (query.length > target.length) return false;
|
|
56
|
-
|
|
57
|
-
let queryIndex = 0;
|
|
58
|
-
for (let targetIndex = 0; targetIndex < target.length && queryIndex < query.length; targetIndex += 1) {
|
|
59
|
-
if (query[queryIndex] === target[targetIndex]) {
|
|
60
|
-
queryIndex += 1;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return queryIndex === query.length;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function fuzzyScore(query: string, target: string): number {
|
|
68
|
-
if (query.length === 0) return 1;
|
|
69
|
-
if (target === query) return 100;
|
|
70
|
-
if (target.startsWith(query)) return 80;
|
|
71
|
-
if (target.includes(query)) return 60;
|
|
72
|
-
|
|
73
|
-
let queryIndex = 0;
|
|
74
|
-
let gaps = 0;
|
|
75
|
-
let lastMatchIndex = -1;
|
|
76
|
-
for (let targetIndex = 0; targetIndex < target.length && queryIndex < query.length; targetIndex += 1) {
|
|
77
|
-
if (query[queryIndex] === target[targetIndex]) {
|
|
78
|
-
if (lastMatchIndex >= 0 && targetIndex - lastMatchIndex > 1) {
|
|
79
|
-
gaps += 1;
|
|
80
|
-
}
|
|
81
|
-
lastMatchIndex = targetIndex;
|
|
82
|
-
queryIndex += 1;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (queryIndex !== query.length) return 0;
|
|
87
|
-
return Math.max(1, 40 - gaps * 5);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
56
|
function isPromptActionItem(item: AutocompleteItem): item is PromptActionAutocompleteItem {
|
|
91
57
|
return "actionId" in item && "execute" in item && typeof item.execute === "function";
|
|
92
58
|
}
|
|
@@ -127,6 +93,7 @@ function sortSlashCommandSuggestions(
|
|
|
127
93
|
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
128
94
|
if (!suggestions) return null;
|
|
129
95
|
const query = suggestions.prefix.slice(1).toLowerCase();
|
|
96
|
+
const normalizedQuery = normalizeFuzzyText(query);
|
|
130
97
|
const commandIndexes = new Map(commands.map((command, index) => [command.name, index]));
|
|
131
98
|
const commandByName = new Map(commands.map(command => [command.name, command]));
|
|
132
99
|
const items = suggestions.items
|
|
@@ -135,13 +102,20 @@ function sortSlashCommandSuggestions(
|
|
|
135
102
|
const commandIndex = commandIndexes.get(item.value) ?? index;
|
|
136
103
|
const lowerName = item.value.toLowerCase();
|
|
137
104
|
const lowerDesc = command?.description?.toLowerCase() ?? item.description?.toLowerCase() ?? "";
|
|
138
|
-
const
|
|
139
|
-
const
|
|
105
|
+
const normalizedName = normalizeFuzzyText(item.value);
|
|
106
|
+
const normalizedDesc = normalizeFuzzyText(lowerDesc);
|
|
107
|
+
const nameScore = autocompleteFuzzyMatch(normalizedQuery, normalizedName)
|
|
108
|
+
? autocompleteFuzzyScore(normalizedQuery, normalizedName)
|
|
109
|
+
: 0;
|
|
110
|
+
const descScore = autocompleteFuzzyMatch(normalizedQuery, normalizedDesc)
|
|
111
|
+
? autocompleteFuzzyScore(normalizedQuery, normalizedDesc) * 0.5
|
|
112
|
+
: 0;
|
|
113
|
+
const isAscii = /^[\x00-\x7F]*$/.test(`${query}${item.value}`);
|
|
140
114
|
return {
|
|
141
115
|
item,
|
|
142
116
|
index,
|
|
143
117
|
commandIndex,
|
|
144
|
-
matchRank: getSlashCommandMatchRank(query, lowerName),
|
|
118
|
+
matchRank: isAscii ? getSlashCommandMatchRank(query, lowerName) : 4,
|
|
145
119
|
priority: getSlashCommandPriority(command, item),
|
|
146
120
|
score: Math.max(nameScore, descScore),
|
|
147
121
|
};
|
|
@@ -219,14 +193,14 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
219
193
|
const searchable = [action.id, action.label, action.description, ...action.keywords]
|
|
220
194
|
.join(" ")
|
|
221
195
|
.toLowerCase();
|
|
222
|
-
if (!
|
|
196
|
+
if (!autocompleteFuzzyMatch(query, searchable)) return null;
|
|
223
197
|
return {
|
|
224
198
|
value: action.label,
|
|
225
199
|
label: action.label,
|
|
226
200
|
description: action.description,
|
|
227
201
|
actionId: action.id,
|
|
228
202
|
execute: action.execute,
|
|
229
|
-
score:
|
|
203
|
+
score: autocompleteFuzzyScore(query, searchable),
|
|
230
204
|
} satisfies PromptActionAutocompleteItem & { score: number };
|
|
231
205
|
})
|
|
232
206
|
.filter(item => item !== null)
|
|
@@ -350,7 +324,10 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
350
324
|
if (!prefix) return null;
|
|
351
325
|
const query = prefix.slice(1).toLowerCase();
|
|
352
326
|
if (query.length === 0 && !options.includeEmpty) return null;
|
|
353
|
-
const normalizedQuery =
|
|
327
|
+
const normalizedQuery = normalizeFuzzyText(
|
|
328
|
+
query.startsWith("skill-") ? `skill:${query.slice("skill-".length)}` : query,
|
|
329
|
+
);
|
|
330
|
+
if (query.length > 0 && normalizedQuery.length === 0) return null;
|
|
354
331
|
const exactNonSkillCommand = this.#commands.some(
|
|
355
332
|
command => command.name === query && !command.name.startsWith("skill:"),
|
|
356
333
|
);
|
|
@@ -364,16 +341,18 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
364
341
|
`skill-${skillName}`,
|
|
365
342
|
...(exactNonSkillCommand ? [] : [skillName]),
|
|
366
343
|
command.description ?? "",
|
|
367
|
-
].map(target => target
|
|
344
|
+
].map(target => normalizeFuzzyText(target));
|
|
368
345
|
if (
|
|
369
|
-
!searchTargets.some(
|
|
346
|
+
!searchTargets.some(
|
|
347
|
+
target => autocompleteFuzzyMatch(normalizedQuery, target) || target.includes(normalizedQuery),
|
|
348
|
+
)
|
|
370
349
|
) {
|
|
371
350
|
return null;
|
|
372
351
|
}
|
|
373
352
|
const bestScore = Math.max(
|
|
374
353
|
...searchTargets.map(target =>
|
|
375
|
-
|
|
376
|
-
?
|
|
354
|
+
autocompleteFuzzyMatch(normalizedQuery, target)
|
|
355
|
+
? autocompleteFuzzyScore(normalizedQuery, target)
|
|
377
356
|
: target.includes(normalizedQuery)
|
|
378
357
|
? 60
|
|
379
358
|
: 0,
|
|
@@ -343,7 +343,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
|
|
|
343
343
|
return { retried: true, immediate: true };
|
|
344
344
|
}
|
|
345
345
|
case "bash.background": {
|
|
346
|
-
if (!session.requestForegroundBashBackground())
|
|
346
|
+
if (!(await session.requestForegroundBashBackground()))
|
|
347
347
|
throw Object.assign(
|
|
348
348
|
new Error("The active bash command cannot be moved to a managed background job."),
|
|
349
349
|
{ code: "not_foldable" },
|
|
@@ -17,7 +17,11 @@ import type { InteractiveModeContext } from "../types";
|
|
|
17
17
|
* (unhandled rejection → resident crash).
|
|
18
18
|
*/
|
|
19
19
|
export function extensionUserMessageContentError(content: string | (TextContent | ImageContent)[]): Error | undefined {
|
|
20
|
-
if (typeof content === "string")
|
|
20
|
+
if (typeof content === "string") {
|
|
21
|
+
if (content.trim().length === 0)
|
|
22
|
+
return Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
21
25
|
if (!Array.isArray(content)) {
|
|
22
26
|
return Object.assign(new Error("sendUserMessage requires string or content-array content."), {
|
|
23
27
|
code: "invalid_input",
|
|
@@ -30,6 +34,16 @@ export function extensionUserMessageContentError(content: string | (TextContent
|
|
|
30
34
|
});
|
|
31
35
|
}
|
|
32
36
|
}
|
|
37
|
+
const text = content
|
|
38
|
+
.filter((part): part is TextContent => part.type === "text")
|
|
39
|
+
.map(part => part.text)
|
|
40
|
+
.join("\n");
|
|
41
|
+
const hasUsableImage = content.some(
|
|
42
|
+
(part): part is ImageContent =>
|
|
43
|
+
part.type === "image" && typeof part.data === "string" && part.data.trim().length > 0,
|
|
44
|
+
);
|
|
45
|
+
if (text.trim().length === 0 && !hasUsableImage)
|
|
46
|
+
return Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
33
47
|
return undefined;
|
|
34
48
|
}
|
|
35
49
|
export function normalizeInjectedUserContent(content: string | (TextContent | ImageContent)[]): {
|
|
@@ -16,8 +16,8 @@ Executes bash command in shell session for terminal operations like git, bun, ca
|
|
|
16
16
|
{{#if asyncEnabled}}
|
|
17
17
|
- Use `async: true` for long-running commands when you don't need immediate output; the call returns a background job ID and the result is delivered automatically as a follow-up.
|
|
18
18
|
{{/if}}
|
|
19
|
-
{{#if
|
|
20
|
-
- In the interactive TUI, the user can press `
|
|
19
|
+
{{#if foregroundFoldEnabled}}
|
|
20
|
+
- In the interactive TUI, the user can press the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via the `app.tool.backgroundFold` keybinding) while a foreground command is still running to fold it into a background job. All three command surfaces fold: managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded command keeps its original timeout and is never killed or restarted by folding; a folded PTY continues output-only, so it stops accepting input. The turn then ends cleanly and a later turn resumes automatically with the command's result, so the interrupted task is finished rather than abandoned. `task`/`subagent` waits are deliberately NOT foldable. Do not instruct users to use raw shell `Ctrl+Z`/`bg` inside the GJC TUI; ownership and output routing are not safe there.
|
|
21
21
|
{{/if}}
|
|
22
22
|
</instruction>
|
|
23
23
|
{{#if restrictedAllowedPrefixes}}
|
package/src/prompts/tools/job.md
CHANGED
|
@@ -2,7 +2,7 @@ Inspects, waits, or cancels async jobs.
|
|
|
2
2
|
|
|
3
3
|
Background job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.
|
|
4
4
|
|
|
5
|
-
In the interactive TUI,
|
|
5
|
+
In the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.
|
|
6
6
|
|
|
7
7
|
# Operations
|
|
8
8
|
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
type SessionReconcileUncertainTarget,
|
|
10
10
|
validateSessionReconcileUncertainTarget,
|
|
11
11
|
} from "../lifecycle/service";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
validateAdapterControl,
|
|
14
|
+
validateAdapterSecretFields,
|
|
15
|
+
validateRequiredPromptText,
|
|
16
|
+
} from "../protocol/adapter-validation";
|
|
13
17
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
14
18
|
import { type SessionAttachment, type SessionRouter, SessionRouterError } from "../router";
|
|
15
19
|
import { ACP_SESSION_RECONNECT, SESSION_ABORT_TIMEOUT_MS } from "../session-reconnect";
|
|
@@ -343,7 +347,18 @@ export class AcpSdkAdapter {
|
|
|
343
347
|
}
|
|
344
348
|
|
|
345
349
|
async prompt(params: JsonObject | string): Promise<unknown> {
|
|
346
|
-
const
|
|
350
|
+
const rawText =
|
|
351
|
+
typeof params === "string"
|
|
352
|
+
? params
|
|
353
|
+
: params !== null && typeof params === "object"
|
|
354
|
+
? (params.prompt ?? params.text)
|
|
355
|
+
: undefined;
|
|
356
|
+
const text = typeof rawText === "string" ? rawText : "";
|
|
357
|
+
const invalid = validateRequiredPromptText("turn.prompt", {
|
|
358
|
+
text,
|
|
359
|
+
...(typeof params === "object" && Array.isArray(params.images) ? { images: params.images } : {}),
|
|
360
|
+
});
|
|
361
|
+
if (invalid) throw new AcpSdkAdapterError(invalid.code, invalid.message);
|
|
347
362
|
return await this.#requestSession({
|
|
348
363
|
type: "control_request",
|
|
349
364
|
operation: "turn.prompt",
|