@gajae-code/coding-agent 0.16.0 → 0.16.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 +41 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/rows.d.ts +8 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/index.d.ts +6 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +154 -28
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +5 -5
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +111 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +33 -21
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +2 -0
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +463 -8
- package/src/sdk/host/session-runtime.ts +383 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +152 -59
- package/src/sdk/session.ts +60 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +156 -33
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/index.ts +6 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
package/src/main.ts
CHANGED
|
@@ -453,6 +453,7 @@ type StartupModelProfileArgs = {
|
|
|
453
453
|
startupModel?: CreateAgentSessionOptions["model"];
|
|
454
454
|
startupThinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
455
455
|
preferCachedModels?: boolean;
|
|
456
|
+
preferCachedDefaultProfile?: boolean;
|
|
456
457
|
};
|
|
457
458
|
|
|
458
459
|
async function applyStartupModelProfilesWithPolicy(
|
|
@@ -484,7 +485,9 @@ async function applyStartupModelProfilesWithPolicy(
|
|
|
484
485
|
// deferred `--model <pattern>` path resolved inside createAgentSession.
|
|
485
486
|
const explicitModel = args.parsedArgs.model ? (args.startupModel ?? args.session.model) : undefined;
|
|
486
487
|
const defaultProfile = args.settings.get("modelProfile.default");
|
|
487
|
-
const preferCachedProfiles =
|
|
488
|
+
const preferCachedProfiles =
|
|
489
|
+
(args.preferCachedModels === true && args.parsedArgs.mpreset !== undefined) ||
|
|
490
|
+
(args.preferCachedDefaultProfile === true && defaultProfile !== undefined);
|
|
488
491
|
const applyConfiguredProfiles = async (): Promise<boolean> => {
|
|
489
492
|
let applied = true;
|
|
490
493
|
if (defaultProfile) {
|
|
@@ -715,11 +715,11 @@ const ACP_CONFIG_OPTIONS = [
|
|
|
715
715
|
],
|
|
716
716
|
},
|
|
717
717
|
{
|
|
718
|
-
id: "
|
|
719
|
-
name: "
|
|
718
|
+
id: "toolInterruptPolicy",
|
|
719
|
+
name: "Tool interrupt policy",
|
|
720
720
|
options: [
|
|
721
|
-
{ value: "
|
|
722
|
-
{ value: "
|
|
721
|
+
{ value: "abort_tools", name: "Abort running tools" },
|
|
722
|
+
{ value: "finish_tools", name: "Finish running tools" },
|
|
723
723
|
],
|
|
724
724
|
},
|
|
725
725
|
] as const;
|
|
@@ -727,7 +727,7 @@ const ACP_CONFIG_OPTIONS = [
|
|
|
727
727
|
const ACP_CONFIG_CONTROL_OPERATIONS: Record<string, string> = {
|
|
728
728
|
steeringMode: "queue.steering_mode.set",
|
|
729
729
|
followUpMode: "queue.follow_up_mode.set",
|
|
730
|
-
|
|
730
|
+
toolInterruptPolicy: "queue.tool_interrupt_policy.set",
|
|
731
731
|
};
|
|
732
732
|
|
|
733
733
|
function configValues(query: unknown): Map<string, string> {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
normalizeTierMap,
|
|
23
23
|
validateAutoroutingSetup,
|
|
24
24
|
} from "../../config/autorouting-contract";
|
|
25
|
+
import type { BillingPath } from "../../config/billing-path";
|
|
25
26
|
import {
|
|
26
27
|
getProxyRoutableProviders,
|
|
27
28
|
inspectProxyProviderId,
|
|
@@ -30,7 +31,6 @@ import {
|
|
|
30
31
|
rewriteSelectorForProxy,
|
|
31
32
|
tryResolveProxyProviderId,
|
|
32
33
|
} from "../../config/model-profile-activation";
|
|
33
|
-
|
|
34
34
|
import { isModelProfileProviderAvailable } from "../../config/model-profile-contract";
|
|
35
35
|
import {
|
|
36
36
|
deriveModelProfileMappedProviders,
|
|
@@ -429,6 +429,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
429
429
|
#imageRoleFilter: boolean = false;
|
|
430
430
|
#smartRoutingPanel?: SmartRoutingPanelComponent;
|
|
431
431
|
#smartRoutingPreviewBuilder?: (draft: AutoroutingSetup) => SmartRoutingPreview;
|
|
432
|
+
#isBillingDismissed?: (key: string) => boolean;
|
|
433
|
+
#markBillingDismissed?: (key: string) => void;
|
|
434
|
+
#pendingBillingNoticeKey?: string;
|
|
432
435
|
|
|
433
436
|
// Tab state
|
|
434
437
|
#providers: ProviderTabState[] = STATIC_PROVIDER_TABS;
|
|
@@ -455,6 +458,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
455
458
|
smartRoutingPreview?: (draft: AutoroutingSetup) => SmartRoutingPreview;
|
|
456
459
|
/** Open the smart-routing panel directly instead of the preset landing. */
|
|
457
460
|
smartRoutingOnly?: boolean;
|
|
461
|
+
/** Test seam: synchronous persisted-dismissal check to avoid async render churn. */
|
|
462
|
+
isBillingDismissed?: (key: string) => boolean;
|
|
463
|
+
/** Test seam: record that a billing notice was shown/dismissed. */
|
|
464
|
+
markBillingDismissed?: (key: string) => void;
|
|
458
465
|
},
|
|
459
466
|
) {
|
|
460
467
|
super();
|
|
@@ -468,6 +475,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
468
475
|
this.#temporaryOnly = options?.temporaryOnly ?? false;
|
|
469
476
|
this.#authSessionId = options?.sessionId;
|
|
470
477
|
this.#smartRoutingPreviewBuilder = options?.smartRoutingPreview;
|
|
478
|
+
this.#isBillingDismissed = (options as any)?.isBillingDismissed;
|
|
479
|
+
this.#markBillingDismissed = (options as any)?.markBillingDismissed;
|
|
471
480
|
this.#currentModel = _currentModel;
|
|
472
481
|
this.#currentThinkingLevel = options?.currentThinkingLevel;
|
|
473
482
|
this.#activeModelProfile = options?.activeModelProfile;
|
|
@@ -1497,6 +1506,25 @@ export class ModelSelectorComponent extends Container {
|
|
|
1497
1506
|
const availableModels = this.#getProfileAvailableModels();
|
|
1498
1507
|
const resolutionRegistry = this.#createProfileResolutionRegistry(availableModels);
|
|
1499
1508
|
const providers = new Set<string>();
|
|
1509
|
+
// Preset groups share selectors heavily (role assignments repeat across
|
|
1510
|
+
// profiles), and each `resolveModelRoleValue` call scans the candidate set
|
|
1511
|
+
// while validating lookup contexts and resolving aliases. Resolve identical
|
|
1512
|
+
// selectors once per refresh; the inputs are shared and only the provider is
|
|
1513
|
+
// needed here. This map is deliberately local to the synchronous, pre-await
|
|
1514
|
+
// snapshot below: catalog/config changes and credential-generation changes
|
|
1515
|
+
// start a new refresh with a new map.
|
|
1516
|
+
const resolvedProviderBySelector = new Map<string, string | undefined>();
|
|
1517
|
+
const resolveSelectorProvider = (selector: string): string | undefined => {
|
|
1518
|
+
if (resolvedProviderBySelector.has(selector)) return resolvedProviderBySelector.get(selector);
|
|
1519
|
+
const provider = resolveModelRoleValue(selector, availableModels, {
|
|
1520
|
+
settings: this.#settings,
|
|
1521
|
+
modelRegistry: this.#modelRegistry,
|
|
1522
|
+
aliasIntent: "preset-equivalent",
|
|
1523
|
+
credentialSessionId: this.#authSessionId,
|
|
1524
|
+
}).model?.provider;
|
|
1525
|
+
resolvedProviderBySelector.set(selector, provider);
|
|
1526
|
+
return provider;
|
|
1527
|
+
};
|
|
1500
1528
|
for (const profiles of this.#getPresetGroups().values()) {
|
|
1501
1529
|
for (const profile of profiles) {
|
|
1502
1530
|
for (const provider of profileRequiredProviders(profile)) providers.add(provider);
|
|
@@ -1516,13 +1544,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
1516
1544
|
];
|
|
1517
1545
|
for (const value of values) {
|
|
1518
1546
|
for (const selector of normalizeModelSelectorValue(value)) {
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
modelRegistry: this.#modelRegistry,
|
|
1522
|
-
aliasIntent: "preset-equivalent",
|
|
1523
|
-
credentialSessionId: this.#authSessionId,
|
|
1524
|
-
}).model;
|
|
1525
|
-
if (resolved) providers.add(resolved.provider);
|
|
1547
|
+
const resolvedProvider = resolveSelectorProvider(selector);
|
|
1548
|
+
if (resolvedProvider !== undefined) providers.add(resolvedProvider);
|
|
1526
1549
|
}
|
|
1527
1550
|
}
|
|
1528
1551
|
}
|
|
@@ -1705,12 +1728,24 @@ export class ModelSelectorComponent extends Container {
|
|
|
1705
1728
|
return lines;
|
|
1706
1729
|
}
|
|
1707
1730
|
|
|
1731
|
+
#billingPathForModel(model: Model): BillingPath | undefined {
|
|
1732
|
+
try {
|
|
1733
|
+
const fn = (
|
|
1734
|
+
this.#modelRegistry as unknown as { getBillingPath?: (m: Model, s?: string) => BillingPath | undefined }
|
|
1735
|
+
).getBillingPath;
|
|
1736
|
+
if (typeof fn === "function") return fn.call(this.#modelRegistry, model, this.#authSessionId);
|
|
1737
|
+
} catch {}
|
|
1738
|
+
return undefined;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1708
1741
|
#formatAssignedModelLabel(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
1709
1742
|
const modelLabel = sanitizeText(`${model.provider}/${model.id}`).replace(/\s+/g, " ").trim();
|
|
1710
1743
|
let label = modelLabel;
|
|
1711
1744
|
if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
|
|
1712
1745
|
label += ` (${getThinkingLevelMetadata(thinkingLevel).label})`;
|
|
1713
1746
|
}
|
|
1747
|
+
const billing = this.#billingPathForModel(model)?.label;
|
|
1748
|
+
if (billing) label += ` [${billing}]`;
|
|
1714
1749
|
return truncateToWidth(label, ROLE_BINDING_MAX_WIDTH);
|
|
1715
1750
|
}
|
|
1716
1751
|
|
|
@@ -1914,6 +1949,27 @@ export class ModelSelectorComponent extends Container {
|
|
|
1914
1949
|
this.#listContainer.addChild(new Spacer(1));
|
|
1915
1950
|
this.#listContainer.addChild(new Text(theme.fg("warning", ` ${this.#presetLoginHint}`), 0, 0));
|
|
1916
1951
|
}
|
|
1952
|
+
// One-time metered billing notice for the currently selected model (first bind per credential source)
|
|
1953
|
+
try {
|
|
1954
|
+
const sel = this.#getSelectedItem?.() as any;
|
|
1955
|
+
const m = sel?.model as any;
|
|
1956
|
+
if (m && this.#viewMode === "models") {
|
|
1957
|
+
const bp = this.#billingPathForModel(m);
|
|
1958
|
+
if (bp && !(this.#isBillingDismissed?.(bp.dismissKey) ?? false)) {
|
|
1959
|
+
this.#listContainer.addChild(new Spacer(1));
|
|
1960
|
+
this.#listContainer.addChild(
|
|
1961
|
+
new Text(
|
|
1962
|
+
theme.fg("warning", ` Billing: ${bp.label} — charges may apply. Press d to dismiss.`),
|
|
1963
|
+
0,
|
|
1964
|
+
0,
|
|
1965
|
+
),
|
|
1966
|
+
);
|
|
1967
|
+
this.#pendingBillingNoticeKey = bp.dismissKey;
|
|
1968
|
+
} else {
|
|
1969
|
+
this.#pendingBillingNoticeKey = undefined;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
} catch {}
|
|
1917
1973
|
const previewProfile = this.#getProfileByName(this.#previewProfileName);
|
|
1918
1974
|
if (previewProfile) this.#renderPresetPreview(previewProfile);
|
|
1919
1975
|
}
|
|
@@ -2075,6 +2131,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
2075
2131
|
) {
|
|
2076
2132
|
roleBadgeTokens.push(theme.icon.fast);
|
|
2077
2133
|
}
|
|
2134
|
+
const billingLabel = this.#billingPathForModel(item.model)?.label;
|
|
2135
|
+
if (billingLabel) roleBadgeTokens.push(theme.fg("warning", `[${billingLabel}]`));
|
|
2078
2136
|
const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
|
|
2079
2137
|
|
|
2080
2138
|
let line = "";
|
|
@@ -2307,6 +2365,16 @@ export class ModelSelectorComponent extends Container {
|
|
|
2307
2365
|
return;
|
|
2308
2366
|
}
|
|
2309
2367
|
|
|
2368
|
+
if (keyData === "d" || keyData === "D") {
|
|
2369
|
+
if (this.#pendingBillingNoticeKey) {
|
|
2370
|
+
try {
|
|
2371
|
+
this.#markBillingDismissed?.(this.#pendingBillingNoticeKey);
|
|
2372
|
+
} catch {}
|
|
2373
|
+
this.#pendingBillingNoticeKey = undefined;
|
|
2374
|
+
this.#updateList();
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2310
2378
|
// Pass everything else to search input
|
|
2311
2379
|
this.#searchInput.handleInput(keyData);
|
|
2312
2380
|
this.#filterModels(this.#searchInput.getValue());
|
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
import { formatBytes } from "@gajae-code/utils";
|
|
15
15
|
import { theme } from "../../modes/theme/theme";
|
|
16
16
|
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
|
17
|
-
import
|
|
17
|
+
import {
|
|
18
|
+
prioritizeStarredSessions,
|
|
19
|
+
type ResumeSessionIdentity,
|
|
20
|
+
type ResumeTailInspection,
|
|
21
|
+
type SessionInfo,
|
|
22
|
+
} from "../../session/session-manager";
|
|
18
23
|
import { DynamicBorder } from "./dynamic-border";
|
|
19
24
|
import { HookSelectorComponent } from "./hook-selector";
|
|
20
25
|
|
|
@@ -71,17 +76,19 @@ class SessionList implements Component {
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
#filterSessions(query: string): void {
|
|
74
|
-
this.#filteredSessions =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
this.#filteredSessions = prioritizeStarredSessions(
|
|
80
|
+
fuzzyFilter(this.allSessions, query, session =>
|
|
81
|
+
[
|
|
82
|
+
session.id,
|
|
83
|
+
session.title ?? "",
|
|
84
|
+
session.cwd ?? "",
|
|
85
|
+
session.firstMessage ?? "",
|
|
86
|
+
session.allMessagesText,
|
|
87
|
+
session.path,
|
|
88
|
+
]
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join(" "),
|
|
91
|
+
),
|
|
85
92
|
);
|
|
86
93
|
this.#clampSelectedIndex();
|
|
87
94
|
}
|
|
@@ -131,8 +138,9 @@ class SessionList implements Component {
|
|
|
131
138
|
const cursorWidth = visibleWidth(cursor);
|
|
132
139
|
const prefix = selected ? theme.fg("accent", cursor) : padding(cursorWidth);
|
|
133
140
|
const message = session.firstMessage.replace(/\n/g, " ").trim();
|
|
134
|
-
const
|
|
135
|
-
|
|
141
|
+
const star = session.starred ? theme.fg("warning", `${theme.icon.star} `) : "";
|
|
142
|
+
const title = truncateToWidth(session.title ?? message, width - cursorWidth - visibleWidth(star));
|
|
143
|
+
lines.push(prefix + star + (selected ? theme.bold(title) : title));
|
|
136
144
|
if (session.title) lines.push(` ${theme.fg("dim", truncateToWidth(message, width - cursorWidth))}`);
|
|
137
145
|
lines.push(
|
|
138
146
|
theme.fg(
|
|
@@ -211,7 +219,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
211
219
|
this.addChild(new DynamicBorder());
|
|
212
220
|
this.addChild(new Spacer(1));
|
|
213
221
|
this.addChild(this.#messageContainer);
|
|
214
|
-
this.#sessionList = new SessionList(sessions);
|
|
222
|
+
this.#sessionList = new SessionList(prioritizeStarredSessions(sessions));
|
|
215
223
|
this.#sessionList.onSelect = session => {
|
|
216
224
|
if (this.inspector) void this.#inspect(session);
|
|
217
225
|
else this.#dispatchSelect(session.path);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import { type Component, Container, matchesKey, truncateToWidth } from "@gajae-code/tui";
|
|
3
|
-
import type
|
|
2
|
+
import { type Component, Container, matchesKey, truncateToWidth, visibleWidth } from "@gajae-code/tui";
|
|
3
|
+
import { prioritizeStarredSessions, type SessionInfo } from "../../session/session-manager";
|
|
4
4
|
import { theme } from "../theme/theme";
|
|
5
5
|
|
|
6
6
|
const PRESENCE_MAX_BYTES = 4096;
|
|
@@ -41,6 +41,7 @@ export interface DashboardSession {
|
|
|
41
41
|
id: string;
|
|
42
42
|
cwd: string;
|
|
43
43
|
title: string;
|
|
44
|
+
starred: boolean;
|
|
44
45
|
modified: Date;
|
|
45
46
|
messageCount: number;
|
|
46
47
|
messageCountIsEstimate?: boolean;
|
|
@@ -78,10 +79,11 @@ export function dashboardSessions(
|
|
|
78
79
|
sessions: readonly SessionInfo[],
|
|
79
80
|
options: { readFile?: (filePath: string) => string; now?: number } = {},
|
|
80
81
|
): DashboardSession[] {
|
|
81
|
-
return sessions.map(session => ({
|
|
82
|
+
return prioritizeStarredSessions(sessions).map(session => ({
|
|
82
83
|
id: session.id,
|
|
83
84
|
cwd: session.cwd,
|
|
84
85
|
title: session.title ?? session.firstMessage,
|
|
86
|
+
starred: session.starred,
|
|
85
87
|
modified: session.modified,
|
|
86
88
|
messageCountIsEstimate: session.messageCountIsEstimate,
|
|
87
89
|
messageCount: session.messageCount,
|
|
@@ -169,10 +171,11 @@ class SessionDashboardRow implements Component {
|
|
|
169
171
|
const fullMetadata = `· ${count} · ${modified}`;
|
|
170
172
|
const compactMetadata = `· ${this.session.messageCountIsEstimate ? `~${this.session.messageCount}` : this.session.messageCount} msgs`;
|
|
171
173
|
const metadata = width >= 48 ? fullMetadata : width >= 24 ? compactMetadata : "";
|
|
172
|
-
const
|
|
174
|
+
const star = this.session.starred ? theme.fg("warning", `${theme.icon.star} `) : "";
|
|
175
|
+
const titleWidth = Math.max(0, width - 2 - visibleWidth(star) - metadata.length - (metadata ? 1 : 0));
|
|
173
176
|
return [
|
|
174
177
|
fit(
|
|
175
|
-
` ${status} ${truncateToWidth(title, titleWidth)}${metadata ? ` ${theme.fg("dim", metadata)}` : ""}`,
|
|
178
|
+
` ${status} ${star}${truncateToWidth(title, titleWidth)}${metadata ? ` ${theme.fg("dim", metadata)}` : ""}`,
|
|
176
179
|
width,
|
|
177
180
|
),
|
|
178
181
|
fit(` ${theme.fg("dim", cwd)}`, width),
|
|
@@ -1161,7 +1161,7 @@ export class CommandController {
|
|
|
1161
1161
|
}
|
|
1162
1162
|
|
|
1163
1163
|
try {
|
|
1164
|
-
await this.ctx.sessionManager.
|
|
1164
|
+
await this.ctx.sessionManager.runExclusiveCwdMoveTransition(async () => {
|
|
1165
1165
|
await this.ctx.sessionManager.flush();
|
|
1166
1166
|
await this.ctx.sessionManager.moveTo(resolvedPath);
|
|
1167
1167
|
setProjectDir(resolvedPath);
|
|
@@ -57,6 +57,17 @@ export const __eventControllerPerfCounters = {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS = 10_000;
|
|
60
|
+
const TEXTLESS_LENGTH_WARNING =
|
|
61
|
+
'Response ended with stopReason "length" before producing visible text or a tool call. Check the model\'s output and context token limits (maxTokens in models.yml) or lower the reasoning level (/reasoning), then retry.';
|
|
62
|
+
|
|
63
|
+
function isTextlessLengthStop(message: AssistantMessage | undefined): boolean {
|
|
64
|
+
return (
|
|
65
|
+
message?.stopReason === "length" &&
|
|
66
|
+
!message.content.some(
|
|
67
|
+
block => (block.type === "text" && block.text.trim().length > 0) || block.type === "toolCall",
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
60
71
|
|
|
61
72
|
interface CompletionNotifyPayload {
|
|
62
73
|
type: "agent-turn-complete";
|
|
@@ -904,7 +915,7 @@ export class EventController {
|
|
|
904
915
|
}
|
|
905
916
|
}
|
|
906
917
|
|
|
907
|
-
async #handleAgentEnd(
|
|
918
|
+
async #handleAgentEnd(event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
|
908
919
|
this.ctx.setWorkingMessage(undefined);
|
|
909
920
|
stopInteractiveActivityIndicator(this.ctx, { foregroundSettled: true });
|
|
910
921
|
if (this.ctx.streamingComponent) {
|
|
@@ -929,6 +940,14 @@ export class EventController {
|
|
|
929
940
|
this.ctx.ui.requestRender();
|
|
930
941
|
this.#scheduleIdleCompaction();
|
|
931
942
|
emitHostStatus("finished");
|
|
943
|
+
const lastAssistant = this.ctx.session.getLastAssistantMessage();
|
|
944
|
+
if (
|
|
945
|
+
event.stopReason !== "cancelled" &&
|
|
946
|
+
event.stopReason !== "maintenance" &&
|
|
947
|
+
isTextlessLengthStop(lastAssistant)
|
|
948
|
+
) {
|
|
949
|
+
this.ctx.showWarning(TEXTLESS_LENGTH_WARNING);
|
|
950
|
+
}
|
|
932
951
|
this.sendCompletionNotification();
|
|
933
952
|
this.ctx.promptSuggestion?.onAgentEnd();
|
|
934
953
|
}
|
|
@@ -1204,13 +1223,20 @@ export class EventController {
|
|
|
1204
1223
|
// errored — those are not "Task complete" events. Mirrors the gate
|
|
1205
1224
|
// already used by #currentContextTokens, #handleMessageEnd, and the
|
|
1206
1225
|
// retry / TTSR / compaction skip paths across agent-session.ts.
|
|
1207
|
-
const last = this.ctx.session.getLastAssistantMessage
|
|
1226
|
+
const last = this.ctx.session.getLastAssistantMessage();
|
|
1208
1227
|
if (last?.stopReason === "aborted" || last?.stopReason === "error") return;
|
|
1209
1228
|
|
|
1210
1229
|
const sessionName = this.ctx.sessionManager.getSessionName();
|
|
1211
|
-
const
|
|
1230
|
+
const textlessLengthStop = isTextlessLengthStop(last);
|
|
1231
|
+
const title = textlessLengthStop
|
|
1232
|
+
? sessionName
|
|
1233
|
+
? `${sessionName}: Output limit reached`
|
|
1234
|
+
: "Output limit reached"
|
|
1235
|
+
: sessionName
|
|
1236
|
+
? `${sessionName}: Complete`
|
|
1237
|
+
: "Complete";
|
|
1212
1238
|
const summary = summaryFromMessage(last, 1000);
|
|
1213
|
-
const body = summary ?? "Complete";
|
|
1239
|
+
const body = textlessLengthStop ? TEXTLESS_LENGTH_WARNING : (summary ?? "Complete");
|
|
1214
1240
|
const sessionManager = this.ctx.sessionManager as { getCwd?: () => string; getSessionId?: () => string };
|
|
1215
1241
|
const payload: CompletionNotifyPayload = {
|
|
1216
1242
|
type: "agent-turn-complete",
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
SYNTHETIC_PROVIDER_ID,
|
|
40
40
|
syntheticNamespaceCollision,
|
|
41
41
|
} from "../../sdk/model-profile-model";
|
|
42
|
+
import { bashBackgroundControlError } from "../../session/fold-coordinator";
|
|
42
43
|
import { createReadonlySessionManager } from "../../session/session-manager";
|
|
43
44
|
import { parseThinkingLevel } from "../../thinking";
|
|
44
45
|
import type { TodoPhase } from "../../tools/todo-write";
|
|
@@ -311,12 +312,11 @@ export class ExtensionUiController {
|
|
|
311
312
|
throw Object.assign(new Error("No retry backoff is pending."), { code: "retry_not_pending" });
|
|
312
313
|
session.retryNow();
|
|
313
314
|
return { retried: true, immediate: true };
|
|
314
|
-
case "bash.background":
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return { backgrounded: true };
|
|
315
|
+
case "bash.background": {
|
|
316
|
+
const outcome = await session.requestForegroundBashBackgroundOutcome("sdk_control");
|
|
317
|
+
if (outcome.status !== "folded") throw bashBackgroundControlError(outcome);
|
|
318
|
+
return { backgrounded: true, jobId: outcome.jobId };
|
|
319
|
+
}
|
|
320
320
|
case "compaction.auto.set":
|
|
321
321
|
session.setAutoCompactionEnabled(input.on === true);
|
|
322
322
|
return { changed: true };
|
|
@@ -591,8 +591,8 @@ export class ExtensionUiController {
|
|
|
591
591
|
this.ctx.session.setFollowUpMode(mode);
|
|
592
592
|
return true;
|
|
593
593
|
}
|
|
594
|
-
if (kind === "
|
|
595
|
-
this.ctx.session.
|
|
594
|
+
if (kind === "tool_interrupt" && (mode === "abort_tools" || mode === "finish_tools")) {
|
|
595
|
+
this.ctx.session.setToolInterruptPolicy(mode);
|
|
596
596
|
return true;
|
|
597
597
|
}
|
|
598
598
|
return false;
|
|
@@ -821,8 +821,8 @@ export class ExtensionUiController {
|
|
|
821
821
|
this.ctx.session.setFollowUpMode(mode);
|
|
822
822
|
return true;
|
|
823
823
|
}
|
|
824
|
-
if (kind === "
|
|
825
|
-
this.ctx.session.
|
|
824
|
+
if (kind === "tool_interrupt" && (mode === "abort_tools" || mode === "finish_tools")) {
|
|
825
|
+
this.ctx.session.setToolInterruptPolicy(mode);
|
|
826
826
|
return true;
|
|
827
827
|
}
|
|
828
828
|
return false;
|
|
@@ -962,8 +962,8 @@ export class ExtensionUiController {
|
|
|
962
962
|
this.ctx.session.setFollowUpMode(mode);
|
|
963
963
|
return true;
|
|
964
964
|
}
|
|
965
|
-
if (kind === "
|
|
966
|
-
this.ctx.session.
|
|
965
|
+
if (kind === "tool_interrupt" && (mode === "abort_tools" || mode === "finish_tools")) {
|
|
966
|
+
this.ctx.session.setToolInterruptPolicy(mode);
|
|
967
967
|
return true;
|
|
968
968
|
}
|
|
969
969
|
return false;
|
|
@@ -973,7 +973,7 @@ export class ExtensionUiController {
|
|
|
973
973
|
getConfigItems: () => ({
|
|
974
974
|
steeringMode: this.ctx.session.steeringMode,
|
|
975
975
|
followUpMode: this.ctx.session.followUpMode,
|
|
976
|
-
|
|
976
|
+
toolInterruptPolicy: this.ctx.session.toolInterruptPolicy,
|
|
977
977
|
}),
|
|
978
978
|
getBranchCandidates: () => this.ctx.sessionManager.getTree(),
|
|
979
979
|
getExtensions: () => this.ctx.session.extensionRunner?.getExtensionPaths() ?? [],
|
|
@@ -1170,7 +1170,7 @@ export class InputController {
|
|
|
1170
1170
|
const promptOptions =
|
|
1171
1171
|
streamingBehavior === "followUp"
|
|
1172
1172
|
? { streamingBehavior, images, followUpQueuePolicy: "sequential" as const }
|
|
1173
|
-
: { streamingBehavior, images };
|
|
1173
|
+
: { streamingBehavior, images, steerQueuePolicy: "sequential" as const };
|
|
1174
1174
|
await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, promptOptions), {
|
|
1175
1175
|
imageCount: images?.length ?? 0,
|
|
1176
1176
|
});
|
|
@@ -1683,7 +1683,7 @@ export class InputController {
|
|
|
1683
1683
|
},
|
|
1684
1684
|
streamingBehavior === "followUp"
|
|
1685
1685
|
? { streamingBehavior, followUpQueuePolicy: "sequential" }
|
|
1686
|
-
: { streamingBehavior },
|
|
1686
|
+
: { streamingBehavior, steerQueuePolicy: "sequential" },
|
|
1687
1687
|
);
|
|
1688
1688
|
}
|
|
1689
1689
|
if (this.ctx.session.isStreaming) {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isSqliteCorruptionError,
|
|
8
8
|
isSqliteError,
|
|
9
9
|
type Model,
|
|
10
|
+
OAuthCredentialSelectorError,
|
|
10
11
|
resolveOAuthStorageProvider,
|
|
11
12
|
} from "@gajae-code/ai/core";
|
|
12
13
|
import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
|
|
@@ -2243,8 +2244,8 @@ export class SelectorController {
|
|
|
2243
2244
|
case "followUpMode":
|
|
2244
2245
|
this.ctx.session.setFollowUpMode(value as "all" | "one-at-a-time");
|
|
2245
2246
|
break;
|
|
2246
|
-
case "
|
|
2247
|
-
this.ctx.session.
|
|
2247
|
+
case "toolInterruptPolicy":
|
|
2248
|
+
this.ctx.session.setToolInterruptPolicy(value as "abort_tools" | "finish_tools");
|
|
2248
2249
|
break;
|
|
2249
2250
|
case "thinkingLevel":
|
|
2250
2251
|
case "defaultThinkingLevel":
|
|
@@ -3706,7 +3707,13 @@ export class SelectorController {
|
|
|
3706
3707
|
{
|
|
3707
3708
|
accountProviderId: selectedProviderId,
|
|
3708
3709
|
onAccountSelect: async (selectorValue: AuthCredentialSelector) => {
|
|
3709
|
-
|
|
3710
|
+
try {
|
|
3711
|
+
await this.ctx.session.setCredentialPin(selectedProviderId, selectorValue);
|
|
3712
|
+
} catch (error: unknown) {
|
|
3713
|
+
if (!(error instanceof OAuthCredentialSelectorError)) throw error;
|
|
3714
|
+
this.ctx.showError(error.message);
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3710
3717
|
selector.stopValidation();
|
|
3711
3718
|
done();
|
|
3712
3719
|
this.ctx.showStatus(`Pinned OAuth account for ${selectedProviderId} to this session.`);
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
starReminderLaunchGate,
|
|
46
46
|
} from "../reminders/star-reminder";
|
|
47
47
|
import type { NotificationSessionReconcileResult, NotificationSessionStatus } from "../sdk/bus/session-control";
|
|
48
|
-
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
48
|
+
import type { AgentSession, AgentSessionEvent, AsyncJobSnapshotItem } from "../session/agent-session";
|
|
49
49
|
import type { HistoryStorage } from "../session/history-storage";
|
|
50
50
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
51
51
|
import { getRecentSessions, getSessionMessageEntryId } from "../session/session-manager";
|
|
@@ -186,18 +186,42 @@ export function getComposerPlaceholder(
|
|
|
186
186
|
return buildComposerPlaceholder(keybindings, context, options);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/** Running background work split into the three kinds the activity indicator names. */
|
|
190
|
+
export interface BackgroundActivityTally {
|
|
191
|
+
subagents: number;
|
|
192
|
+
backgroundBash: number;
|
|
193
|
+
monitors: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Classify running jobs for the activity indicator using the three locked
|
|
198
|
+
* predicates: subagents are `task` jobs carrying subagent metadata, monitors
|
|
199
|
+
* are flagged by `metadata.monitor`, and background bash is a `bash` job that
|
|
200
|
+
* is not a monitor. A running job matching none of them (a task job without
|
|
201
|
+
* subagent metadata) is outside this indicator and is not counted.
|
|
202
|
+
*/
|
|
203
|
+
export function tallyBackgroundActivity(running: readonly AsyncJobSnapshotItem[]): BackgroundActivityTally {
|
|
204
|
+
const tally: BackgroundActivityTally = { subagents: 0, backgroundBash: 0, monitors: 0 };
|
|
205
|
+
for (const item of running) {
|
|
206
|
+
if (item.type === "task" && item.metadata?.subagent !== undefined) tally.subagents += 1;
|
|
207
|
+
else if (item.metadata?.monitor === true) tally.monitors += 1;
|
|
208
|
+
else if (item.type === "bash" && item.metadata?.backgrounded === true) tally.backgroundBash += 1;
|
|
209
|
+
}
|
|
210
|
+
return tally;
|
|
211
|
+
}
|
|
212
|
+
|
|
189
213
|
export function resolveActivityIndicatorMessage(
|
|
190
214
|
foregroundActive: boolean,
|
|
191
|
-
|
|
215
|
+
activity: BackgroundActivityTally,
|
|
192
216
|
foregroundMessage: string,
|
|
193
217
|
): string | undefined {
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
return
|
|
218
|
+
const parts: string[] = [];
|
|
219
|
+
if (activity.subagents > 0) parts.push(`${activity.subagents} subagent${activity.subagents === 1 ? "" : "s"}`);
|
|
220
|
+
if (activity.backgroundBash > 0) parts.push(`${activity.backgroundBash} background bash`);
|
|
221
|
+
if (activity.monitors > 0) parts.push(`${activity.monitors} monitor${activity.monitors === 1 ? "" : "s"}`);
|
|
222
|
+
|
|
223
|
+
if (foregroundActive) return parts.length === 0 ? foregroundMessage : `${foregroundMessage} · ${parts.join(", ")}`;
|
|
224
|
+
return parts.length === 0 ? undefined : `Background: ${parts.join(", ")}…`;
|
|
201
225
|
}
|
|
202
226
|
const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
|
|
203
227
|
const COMPOSER_RIGHT_GUTTER_WIDTH = 1;
|
|
@@ -1926,8 +1950,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1926
1950
|
return main && dim ? { main, dim } : undefined;
|
|
1927
1951
|
}
|
|
1928
1952
|
|
|
1929
|
-
#
|
|
1930
|
-
return this.session.getAsyncJobSnapshot()?.running
|
|
1953
|
+
#activeBackgroundActivity(): BackgroundActivityTally {
|
|
1954
|
+
return tallyBackgroundActivity(this.session.getAsyncJobSnapshot()?.running ?? []);
|
|
1931
1955
|
}
|
|
1932
1956
|
|
|
1933
1957
|
#stopLoadingAnimation(): void {
|
|
@@ -1946,7 +1970,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1946
1970
|
}
|
|
1947
1971
|
const message = resolveActivityIndicatorMessage(
|
|
1948
1972
|
foregroundActive,
|
|
1949
|
-
this.#
|
|
1973
|
+
this.#activeBackgroundActivity(),
|
|
1950
1974
|
this.#pendingWorkingMessage ?? this.#defaultWorkingMessage,
|
|
1951
1975
|
);
|
|
1952
1976
|
if (!message) {
|