@gajae-code/coding-agent 0.11.5 → 0.11.7
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 +37 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +67 -7
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
- package/dist/types/main.d.ts +24 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-status-header.d.ts +1 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +28 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
- package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
- package/dist/types/session/session-manager.d.ts +35 -4
- package/dist/types/session/yield-queue.d.ts +8 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +16 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +34 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +42 -9
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profiles.ts +18 -0
- package/src/config/settings-schema.ts +71 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/ultragoal-guard.ts +88 -10
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +87 -28
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +31 -3
- package/src/modes/components/tool-status-header.ts +6 -1
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- package/src/modes/controllers/input-controller.ts +31 -0
- package/src/modes/controllers/selector-controller.ts +106 -12
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/runtime-init.ts +16 -2
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/transports/http.ts +50 -16
- package/src/sdk/bus/chat-daemon-control.ts +20 -7
- package/src/sdk/bus/index.ts +8 -7
- package/src/sdk/bus/lifecycle-commands.ts +16 -7
- package/src/sdk/bus/notification-service.ts +41 -18
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +26 -8
- package/src/sdk/bus/telegram-daemon.ts +1736 -338
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/session.ts +29 -5
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +1212 -768
- package/src/session/blob-store.ts +19 -6
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +313 -57
- package/src/session/internal/managed-session-storage.ts +146 -35
- package/src/session/session-manager.ts +385 -85
- package/src/session/yield-queue.ts +20 -0
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +37 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/tools/ask-answer-registry.ts +15 -1
- package/src/tools/ask.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +301 -20
- package/src/tools/read.ts +2 -0
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +138 -27
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -169,6 +169,26 @@ async function snapshotDirectory(directoryPath: string, relativePath: string): P
|
|
|
169
169
|
return { relativePath, dev: stat.dev, ino: stat.ino, size: stat.size };
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
interface CanonicalLegacyRoot {
|
|
173
|
+
readonly root: string;
|
|
174
|
+
readonly identity: LegacyEntrySnapshot;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function canonicalLegacyRoot(artifactsDir: string): Promise<CanonicalLegacyRoot> {
|
|
178
|
+
const lexicalRoot = path.resolve(artifactsDir, "local");
|
|
179
|
+
const lexicalIdentity = await snapshotDirectory(lexicalRoot, "");
|
|
180
|
+
const [canonicalArtifactsDir, canonicalRoot] = await Promise.all([
|
|
181
|
+
fs.realpath(path.resolve(artifactsDir)),
|
|
182
|
+
fs.realpath(lexicalRoot),
|
|
183
|
+
]);
|
|
184
|
+
if (canonicalRoot === canonicalArtifactsDir) throw new Error("Unsafe legacy local:// migration source");
|
|
185
|
+
ensureWithinRoot(canonicalRoot, canonicalArtifactsDir);
|
|
186
|
+
const canonicalIdentity = await snapshotDirectory(canonicalRoot, "");
|
|
187
|
+
if (!matchesSnapshot(canonicalIdentity, lexicalIdentity))
|
|
188
|
+
throw new Error("Legacy local:// migration source changed during capture");
|
|
189
|
+
return { root: canonicalRoot, identity: lexicalIdentity };
|
|
190
|
+
}
|
|
191
|
+
|
|
172
192
|
async function snapshotRegularFile(filePath: string, relativePath: string): Promise<LegacyEntrySnapshot> {
|
|
173
193
|
const before = await fs.lstat(filePath, { bigint: true });
|
|
174
194
|
if (!before.isFile() || before.isSymbolicLink()) throw new Error("Unsafe legacy local:// migration source");
|
|
@@ -202,11 +222,16 @@ function manifestsMatch(left: readonly LegacyEntrySnapshot[], right: readonly Le
|
|
|
202
222
|
);
|
|
203
223
|
}
|
|
204
224
|
|
|
205
|
-
async function captureLegacyManifest(
|
|
225
|
+
async function captureLegacyManifest(
|
|
226
|
+
root: string,
|
|
227
|
+
expectedRootIdentity: LegacyEntrySnapshot,
|
|
228
|
+
): Promise<readonly LegacyEntrySnapshot[]> {
|
|
206
229
|
const manifest: LegacyEntrySnapshot[] = [];
|
|
207
230
|
let copiedBytes = 0n;
|
|
208
231
|
const captureDirectory = async (directoryPath: string, relativePath: string): Promise<void> => {
|
|
209
232
|
const directory = await snapshotDirectory(directoryPath, relativePath);
|
|
233
|
+
if (relativePath === "" && !matchesSnapshot(directory, expectedRootIdentity))
|
|
234
|
+
throw new Error("Legacy local:// migration source changed during capture");
|
|
210
235
|
manifest.push(directory);
|
|
211
236
|
const entries = (await fs.readdir(directoryPath, { withFileTypes: true })).sort((a, b) =>
|
|
212
237
|
a.name.localeCompare(b.name),
|
|
@@ -384,10 +409,11 @@ async function migrateLegacyLocal(
|
|
|
384
409
|
await fs.writeFile(marker, "absent\n", { mode: 0o600, flag: "wx" });
|
|
385
410
|
return;
|
|
386
411
|
}
|
|
387
|
-
|
|
412
|
+
let legacySource: CanonicalLegacyRoot;
|
|
388
413
|
let manifest: readonly LegacyEntrySnapshot[];
|
|
389
414
|
try {
|
|
390
|
-
|
|
415
|
+
legacySource = await canonicalLegacyRoot(artifactsDir);
|
|
416
|
+
manifest = await captureLegacyManifest(legacySource.root, legacySource.identity);
|
|
391
417
|
} catch (error) {
|
|
392
418
|
if (isEnoent(error)) {
|
|
393
419
|
await fs.writeFile(marker, "absent\n", { mode: 0o600, flag: "wx" });
|
|
@@ -397,8 +423,8 @@ async function migrateLegacyLocal(
|
|
|
397
423
|
}
|
|
398
424
|
const staging = path.join(scratchParent, `.gjc-local-migration-${randomUUID()}`);
|
|
399
425
|
try {
|
|
400
|
-
await copyLegacyManifest(
|
|
401
|
-
const verifiedManifest = await captureLegacyManifest(
|
|
426
|
+
await copyLegacyManifest(legacySource.root, staging, manifest);
|
|
427
|
+
const verifiedManifest = await captureLegacyManifest(legacySource.root, legacySource.identity);
|
|
402
428
|
if (!manifestsMatch(verifiedManifest, manifest))
|
|
403
429
|
throw new Error("Legacy local:// migration source changed during capture");
|
|
404
430
|
for (const entry of await fs.readdir(staging)) {
|
|
@@ -410,7 +436,7 @@ async function migrateLegacyLocal(
|
|
|
410
436
|
}
|
|
411
437
|
await fs.rename(path.join(staging, entry), path.join(localRoot, entry));
|
|
412
438
|
}
|
|
413
|
-
await retireLegacyTree(
|
|
439
|
+
await retireLegacyTree(legacySource.root, manifest);
|
|
414
440
|
await fs.writeFile(marker, "verified\n", { mode: 0o600, flag: "wx" });
|
|
415
441
|
} finally {
|
|
416
442
|
await fs.rm(staging, { recursive: true, force: true });
|
package/src/main.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { createInterface } from "node:readline/promises";
|
|
|
12
12
|
import type { ImageContent } from "@gajae-code/ai";
|
|
13
13
|
import {
|
|
14
14
|
$env,
|
|
15
|
+
$pickenv,
|
|
16
|
+
getAgentDir,
|
|
15
17
|
getProjectDir,
|
|
16
18
|
logger,
|
|
17
19
|
normalizePathForComparison,
|
|
@@ -20,7 +22,6 @@ import {
|
|
|
20
22
|
VERSION,
|
|
21
23
|
} from "@gajae-code/utils";
|
|
22
24
|
import chalk from "chalk";
|
|
23
|
-
import { runCli } from "./cli";
|
|
24
25
|
import type { Args } from "./cli/args";
|
|
25
26
|
import { processFileArguments } from "./cli/file-processor";
|
|
26
27
|
import { buildInitialMessage } from "./cli/initial-message";
|
|
@@ -535,7 +536,16 @@ interface InteractiveModeFactoryOptions {
|
|
|
535
536
|
type CreateInteractiveMode = (options: InteractiveModeFactoryOptions) => InteractiveMode;
|
|
536
537
|
|
|
537
538
|
type ResumePickerTerminalCheck = () => boolean;
|
|
538
|
-
type ListForResumePickerReadOnly = (cwd: string, sessionDir?: string) => Promise<SessionInfo[]>;
|
|
539
|
+
type ListForResumePickerReadOnly = (cwd: string, sessionDir?: string, storage?: undefined) => Promise<SessionInfo[]>;
|
|
540
|
+
|
|
541
|
+
type ListManagedForResumePickerReadOnly = (
|
|
542
|
+
cwd: string,
|
|
543
|
+
managedAgentDir?: string,
|
|
544
|
+
storage?: undefined,
|
|
545
|
+
) => Promise<SessionInfo[]>;
|
|
546
|
+
|
|
547
|
+
type ResolveManagedAgentDirForScope = (cwd: string) => string;
|
|
548
|
+
|
|
539
549
|
type SelectResumeSession = (sessions: SessionInfo[]) => Promise<SessionSelectionResult>;
|
|
540
550
|
type OpenExistingSessionStrict = (
|
|
541
551
|
identity: ResumeSessionIdentity,
|
|
@@ -547,6 +557,11 @@ type OpenExistingSessionStrict = (
|
|
|
547
557
|
export const BARE_RESUME_CONFLICT_ERROR =
|
|
548
558
|
"--resume without a session cannot be combined with --continue, --fork, or --no-session.";
|
|
549
559
|
export const BARE_RESUME_INTERACTIVE_ERROR = "--resume requires an interactive terminal; use --resume <id>.";
|
|
560
|
+
|
|
561
|
+
/** Resolves only the managed agent root needed for pre-consent picker inventory. */
|
|
562
|
+
export function resolveManagedAgentDirForScope(_cwd: string): string {
|
|
563
|
+
return getAgentDir();
|
|
564
|
+
}
|
|
550
565
|
export const BARE_RESUME_OPEN_ERROR = "Could not open the selected session. Use --resume <id>.";
|
|
551
566
|
|
|
552
567
|
function isBareResume(parsed: Args): boolean {
|
|
@@ -742,7 +757,13 @@ export async function createSessionManager(
|
|
|
742
757
|
if (forkSource.includes("/") || forkSource.includes("\\") || forkSource.endsWith(".jsonl")) {
|
|
743
758
|
return await SessionManager.forkFrom(forkSource, cwd, sessionDestination(), undefined, migrationPolicy);
|
|
744
759
|
}
|
|
745
|
-
const match = await resolveResumableSession(
|
|
760
|
+
const match = await resolveResumableSession(
|
|
761
|
+
forkSource,
|
|
762
|
+
cwd,
|
|
763
|
+
parsed.sessionDir,
|
|
764
|
+
undefined,
|
|
765
|
+
parsed.sessionDir ? undefined : activeSettings.getAgentDir(),
|
|
766
|
+
);
|
|
746
767
|
if (!match) {
|
|
747
768
|
throw new Error(`Session "${forkSource}" not found.`);
|
|
748
769
|
}
|
|
@@ -755,14 +776,18 @@ export async function createSessionManager(
|
|
|
755
776
|
if (typeof parsed.resume === "string") {
|
|
756
777
|
const sessionArg = parsed.resume;
|
|
757
778
|
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
SessionManager.explicitDestination(path.dirname(sessionArg))
|
|
761
|
-
|
|
762
|
-
migrationPolicy,
|
|
763
|
-
);
|
|
779
|
+
const destination = parsed.sessionDir
|
|
780
|
+
? SessionManager.explicitDestination(parsed.sessionDir)
|
|
781
|
+
: SessionManager.explicitDestination(path.dirname(sessionArg));
|
|
782
|
+
return await SessionManager.open(sessionArg, destination, undefined, migrationPolicy);
|
|
764
783
|
}
|
|
765
|
-
const match = await resolveResumableSession(
|
|
784
|
+
const match = await resolveResumableSession(
|
|
785
|
+
sessionArg,
|
|
786
|
+
cwd,
|
|
787
|
+
parsed.sessionDir,
|
|
788
|
+
undefined,
|
|
789
|
+
parsed.sessionDir ? undefined : activeSettings.getAgentDir(),
|
|
790
|
+
);
|
|
766
791
|
if (!match) {
|
|
767
792
|
throw new Error(`Session "${sessionArg}" not found.`);
|
|
768
793
|
}
|
|
@@ -1059,12 +1084,41 @@ export interface RunRootCommandDependencies {
|
|
|
1059
1084
|
runPrintMode?: RunPrintMode;
|
|
1060
1085
|
isResumePickerTerminal?: ResumePickerTerminalCheck;
|
|
1061
1086
|
listForResumePickerReadOnly?: ListForResumePickerReadOnly;
|
|
1087
|
+
listManagedForResumePickerReadOnly?: ListManagedForResumePickerReadOnly;
|
|
1088
|
+
resolveManagedAgentDirForScope?: ResolveManagedAgentDirForScope;
|
|
1062
1089
|
selectResumeSession?: SelectResumeSession;
|
|
1063
1090
|
openExistingSessionStrict?: OpenExistingSessionStrict;
|
|
1064
1091
|
initializeSettings?: typeof Settings.init;
|
|
1065
1092
|
loadSettingsForScope?: typeof Settings.loadForScope;
|
|
1066
1093
|
}
|
|
1067
1094
|
|
|
1095
|
+
export interface ModelRoleOverrides {
|
|
1096
|
+
smol?: string;
|
|
1097
|
+
slow?: string;
|
|
1098
|
+
plan?: string;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Resolve the ephemeral `smol`/`slow`/`plan` model-role overrides.
|
|
1103
|
+
*
|
|
1104
|
+
* Precedence per role is CLI flag > documented `GJC_*_MODEL` > legacy
|
|
1105
|
+
* `PI_*_MODEL`, matching the repo-wide GJC-first/PI-fallback convention.
|
|
1106
|
+
* Resolution reads the process environment via `$pickenv`, which trims values
|
|
1107
|
+
* and treats empty/whitespace as unset; it is deliberately kept separate from
|
|
1108
|
+
* credential env resolution (`$credentialEnv`/`$pickCredentialEnv`). The
|
|
1109
|
+
* function is pure and stateless, so it reads fresh each call and a later
|
|
1110
|
+
* invocation never inherits an earlier one's values.
|
|
1111
|
+
*/
|
|
1112
|
+
export function resolveModelRoleOverrides(parsed: Pick<Args, "smol" | "slow" | "plan">): ModelRoleOverrides {
|
|
1113
|
+
const overrides: ModelRoleOverrides = {};
|
|
1114
|
+
const smol = parsed.smol ?? $pickenv("GJC_SMOL_MODEL", "PI_SMOL_MODEL");
|
|
1115
|
+
const slow = parsed.slow ?? $pickenv("GJC_SLOW_MODEL", "PI_SLOW_MODEL");
|
|
1116
|
+
const plan = parsed.plan ?? $pickenv("GJC_PLAN_MODEL", "PI_PLAN_MODEL");
|
|
1117
|
+
if (smol) overrides.smol = smol;
|
|
1118
|
+
if (slow) overrides.slow = slow;
|
|
1119
|
+
if (plan) overrides.plan = plan;
|
|
1120
|
+
return overrides;
|
|
1121
|
+
}
|
|
1068
1122
|
export async function runRootCommand(
|
|
1069
1123
|
parsed: Args,
|
|
1070
1124
|
rawArgs: string[],
|
|
@@ -1095,10 +1149,18 @@ export async function runRootCommand(
|
|
|
1095
1149
|
await logger.time("maybeAutoChdir", maybeAutoChdir, parsedArgs);
|
|
1096
1150
|
autoChdirApplied = true;
|
|
1097
1151
|
const resumeCwd = getProjectDir();
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1152
|
+
const managedAgentDir = parsedArgs.sessionDir
|
|
1153
|
+
? undefined
|
|
1154
|
+
: (deps.resolveManagedAgentDirForScope ?? resolveManagedAgentDirForScope)(resumeCwd);
|
|
1155
|
+
const sessions = parsedArgs.sessionDir
|
|
1156
|
+
? await (deps.listForResumePickerReadOnly ?? SessionManager.listForResumePickerReadOnly)(
|
|
1157
|
+
resumeCwd,
|
|
1158
|
+
parsedArgs.sessionDir,
|
|
1159
|
+
)
|
|
1160
|
+
: await (deps.listManagedForResumePickerReadOnly ?? SessionManager.listManagedForResumePickerReadOnly)(
|
|
1161
|
+
resumeCwd,
|
|
1162
|
+
managedAgentDir,
|
|
1163
|
+
);
|
|
1102
1164
|
if (sessions.length === 0) {
|
|
1103
1165
|
process.stdout.write(`${chalk.dim("No sessions found")}\n`);
|
|
1104
1166
|
return;
|
|
@@ -1109,19 +1171,16 @@ export async function runRootCommand(
|
|
|
1109
1171
|
if (selection.kind === "cancelled") {
|
|
1110
1172
|
return;
|
|
1111
1173
|
}
|
|
1174
|
+
const scopedSettings = await (deps.loadSettingsForScope ?? Settings.loadForScope)({ cwd: resumeCwd });
|
|
1112
1175
|
const resumeMigrationPolicy =
|
|
1113
|
-
(
|
|
1114
|
-
"session.directoryMigration",
|
|
1115
|
-
) === "disabled"
|
|
1116
|
-
? "disabled"
|
|
1117
|
-
: "copy-retain";
|
|
1176
|
+
scopedSettings.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
|
|
1118
1177
|
let opened: StrictSessionOpenResult;
|
|
1119
1178
|
try {
|
|
1120
1179
|
opened = await (deps.openExistingSessionStrict ?? SessionManager.openExistingStrict)(
|
|
1121
1180
|
selection.identity,
|
|
1122
1181
|
parsedArgs.sessionDir
|
|
1123
1182
|
? SessionManager.explicitDestination(parsedArgs.sessionDir)
|
|
1124
|
-
: SessionManager.managedDestination(resumeCwd,
|
|
1183
|
+
: SessionManager.managedDestination(resumeCwd, scopedSettings.getAgentDir()),
|
|
1125
1184
|
undefined,
|
|
1126
1185
|
resumeMigrationPolicy,
|
|
1127
1186
|
);
|
|
@@ -1253,15 +1312,14 @@ export async function runRootCommand(
|
|
|
1253
1312
|
// Initialize discovery system with settings for provider persistence
|
|
1254
1313
|
logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
|
|
1255
1314
|
|
|
1256
|
-
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
|
|
1257
|
-
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
if (smolModel || slowModel || planModel) {
|
|
1315
|
+
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted).
|
|
1316
|
+
// Precedence per role: CLI flag > documented GJC_*_MODEL > legacy PI_*_MODEL.
|
|
1317
|
+
const roleOverrides = resolveModelRoleOverrides(parsedArgs);
|
|
1318
|
+
if (roleOverrides.smol || roleOverrides.slow || roleOverrides.plan) {
|
|
1261
1319
|
settingsInstance.overrideModelRoles({
|
|
1262
|
-
smol:
|
|
1263
|
-
slow:
|
|
1264
|
-
plan:
|
|
1320
|
+
...(roleOverrides.smol ? { smol: roleOverrides.smol } : {}),
|
|
1321
|
+
...(roleOverrides.slow ? { slow: roleOverrides.slow } : {}),
|
|
1322
|
+
...(roleOverrides.plan ? { plan: roleOverrides.plan } : {}),
|
|
1265
1323
|
});
|
|
1266
1324
|
}
|
|
1267
1325
|
|
|
@@ -1629,5 +1687,6 @@ export async function main(args: string[]): Promise<void> {
|
|
|
1629
1687
|
await completeManagedOwnerRecovery(admission.context);
|
|
1630
1688
|
return;
|
|
1631
1689
|
}
|
|
1690
|
+
const { runCli } = await import("./cli");
|
|
1632
1691
|
await runCli(args.length === 0 ? ["launch"] : args);
|
|
1633
1692
|
}
|
|
@@ -49,7 +49,7 @@ export const APP_ACTION_METADATA: readonly ActionMetadata[] = [
|
|
|
49
49
|
action("app.clipboard.copyPrompt", "Copy prompt", "Clipboard", ["composer"]),
|
|
50
50
|
action("app.session.new", "New session", "Session", ["composer"]),
|
|
51
51
|
action("app.session.tree", "Session tree", "Session", ["composer"]),
|
|
52
|
-
action("app.session.fork", "
|
|
52
|
+
action("app.session.fork", "Branch from message", "Session", ["composer"]),
|
|
53
53
|
action("app.session.resume", "Resume session", "Session", ["composer"]),
|
|
54
54
|
action("app.session.observe", "Observe sessions", "Session", ["composer"]),
|
|
55
55
|
action("app.session.dashboard", "Show sessions dashboard", "Session", ["composer"]),
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type Component, Container, Markdown, Spacer, Text, type TUI } from "@gajae-code/tui";
|
|
2
|
+
import {
|
|
3
|
+
BTW_MAX_ANSWER_UTF8_BYTES,
|
|
4
|
+
BTW_MAX_CONTEXT_TURNS,
|
|
5
|
+
sanitizeBtwError,
|
|
6
|
+
truncateUtf8,
|
|
7
|
+
} from "../../session/btw-contract";
|
|
2
8
|
import { replaceTabs } from "../../tools/render-utils";
|
|
3
9
|
import { getMarkdownTheme, theme } from "../theme/theme";
|
|
4
10
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -10,12 +16,21 @@ interface BtwPanelComponentOptions {
|
|
|
10
16
|
tui: TUI;
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
interface BtwTurn {
|
|
20
|
+
question: string;
|
|
21
|
+
answer: string;
|
|
22
|
+
state: BtwPanelState;
|
|
23
|
+
errorMessage?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
export class BtwPanelComponent extends Container {
|
|
14
27
|
#question: string;
|
|
15
28
|
#tui: TUI;
|
|
16
29
|
#state: BtwPanelState = "running";
|
|
17
30
|
#answer = "";
|
|
18
31
|
#errorMessage: string | undefined;
|
|
32
|
+
#completedTurns: BtwTurn[] = [];
|
|
33
|
+
#streamingContent = new Container();
|
|
19
34
|
#closed = false;
|
|
20
35
|
|
|
21
36
|
constructor(options: BtwPanelComponentOptions) {
|
|
@@ -25,16 +40,29 @@ export class BtwPanelComponent extends Container {
|
|
|
25
40
|
this.#rebuild();
|
|
26
41
|
}
|
|
27
42
|
|
|
43
|
+
beginTurn(question: string): void {
|
|
44
|
+
if (this.#closed) return;
|
|
45
|
+
if (this.#state === "complete") {
|
|
46
|
+
this.#completedTurns.push(this.#currentTurn());
|
|
47
|
+
if (this.#completedTurns.length > BTW_MAX_CONTEXT_TURNS) this.#completedTurns.shift();
|
|
48
|
+
}
|
|
49
|
+
this.#question = question;
|
|
50
|
+
this.#answer = "";
|
|
51
|
+
this.#errorMessage = undefined;
|
|
52
|
+
this.#state = "running";
|
|
53
|
+
this.#rebuild();
|
|
54
|
+
}
|
|
55
|
+
|
|
28
56
|
appendText(delta: string): void {
|
|
29
57
|
if (!delta || this.#closed) return;
|
|
30
|
-
this.#answer
|
|
31
|
-
this.#
|
|
58
|
+
this.#answer = truncateUtf8(this.#answer + delta, BTW_MAX_ANSWER_UTF8_BYTES);
|
|
59
|
+
this.#updateStreamingContent();
|
|
32
60
|
}
|
|
33
61
|
|
|
34
62
|
setAnswer(text: string): void {
|
|
35
63
|
if (this.#closed) return;
|
|
36
|
-
this.#answer = text;
|
|
37
|
-
this.#
|
|
64
|
+
this.#answer = truncateUtf8(text, BTW_MAX_ANSWER_UTF8_BYTES);
|
|
65
|
+
this.#updateStreamingContent();
|
|
38
66
|
}
|
|
39
67
|
|
|
40
68
|
markComplete(): void {
|
|
@@ -54,21 +82,33 @@ export class BtwPanelComponent extends Container {
|
|
|
54
82
|
markError(message: string): void {
|
|
55
83
|
if (this.#closed) return;
|
|
56
84
|
this.#state = "error";
|
|
57
|
-
this.#
|
|
85
|
+
this.#answer = "";
|
|
86
|
+
this.#errorMessage = sanitizeBtwError(message);
|
|
58
87
|
this.#rebuild();
|
|
59
88
|
}
|
|
60
89
|
|
|
61
90
|
close(): void {
|
|
62
91
|
this.#closed = true;
|
|
92
|
+
this.#question = "";
|
|
93
|
+
this.#completedTurns = [];
|
|
94
|
+
this.#answer = "";
|
|
95
|
+
this.#errorMessage = undefined;
|
|
96
|
+
this.#streamingContent.clear();
|
|
97
|
+
this.clear();
|
|
63
98
|
}
|
|
64
99
|
|
|
65
100
|
#rebuild(): void {
|
|
66
101
|
this.clear();
|
|
67
102
|
this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
|
|
68
103
|
this.addChild(new Spacer(1));
|
|
104
|
+
for (const turn of this.#completedTurns) {
|
|
105
|
+
this.addChild(this.#turnComponent(turn));
|
|
106
|
+
this.addChild(new Spacer(1));
|
|
107
|
+
}
|
|
69
108
|
this.addChild(new Text(theme.fg("accent", replaceTabs(this.#question)), 1, 0));
|
|
70
109
|
this.addChild(new Spacer(1));
|
|
71
|
-
this
|
|
110
|
+
this.#updateStreamingContent(false);
|
|
111
|
+
this.addChild(this.#streamingContent);
|
|
72
112
|
this.addChild(new Spacer(1));
|
|
73
113
|
this.addChild(new Text(this.#footerLine(), 1, 0));
|
|
74
114
|
this.addChild(new Spacer(1));
|
|
@@ -76,29 +116,55 @@ export class BtwPanelComponent extends Container {
|
|
|
76
116
|
this.#tui.requestRender();
|
|
77
117
|
}
|
|
78
118
|
|
|
119
|
+
#updateStreamingContent(requestRender = true): void {
|
|
120
|
+
this.#streamingContent.clear();
|
|
121
|
+
this.#streamingContent.addChild(this.#contentComponent(this.#state, this.#answer, this.#errorMessage));
|
|
122
|
+
if (requestRender) this.#tui.requestRender();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#currentTurn(): BtwTurn {
|
|
126
|
+
return {
|
|
127
|
+
question: this.#question,
|
|
128
|
+
answer: this.#answer,
|
|
129
|
+
state: this.#state,
|
|
130
|
+
errorMessage: this.#errorMessage,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#turnComponent(turn: BtwTurn): Component {
|
|
135
|
+
const container = new Container();
|
|
136
|
+
container.addChild(new Text(theme.fg("accent", replaceTabs(turn.question)), 1, 0));
|
|
137
|
+
container.addChild(new Spacer(1));
|
|
138
|
+
container.addChild(this.#contentComponent(turn.state, turn.answer, turn.errorMessage));
|
|
139
|
+
return container;
|
|
140
|
+
}
|
|
141
|
+
|
|
79
142
|
#footerLine(): string {
|
|
80
143
|
switch (this.#state) {
|
|
81
144
|
case "running":
|
|
82
|
-
return theme.fg("muted", "Esc cancel /btw");
|
|
145
|
+
return theme.fg("muted", "Esc cancel /btw and return to main chat");
|
|
83
146
|
case "complete":
|
|
84
|
-
return theme.fg("muted", "Esc
|
|
147
|
+
return theme.fg("muted", "Type a follow-up · Esc return to main chat");
|
|
85
148
|
case "aborted":
|
|
86
|
-
return theme.fg(
|
|
149
|
+
return theme.fg(
|
|
150
|
+
"warning",
|
|
151
|
+
`${theme.status.warning} Cancelled · Type a follow-up · Esc return to main chat`,
|
|
152
|
+
);
|
|
87
153
|
case "error":
|
|
88
|
-
return theme.fg("error", `${theme.status.error} Error · Esc
|
|
154
|
+
return theme.fg("error", `${theme.status.error} Error · Type a follow-up · Esc return to main chat`);
|
|
89
155
|
}
|
|
90
156
|
}
|
|
91
157
|
|
|
92
|
-
#contentComponent(): Component {
|
|
93
|
-
if (
|
|
94
|
-
return new Text(theme.fg("error", replaceTabs(
|
|
158
|
+
#contentComponent(state: BtwPanelState, answer: string, errorMessage: string | undefined): Component {
|
|
159
|
+
if (state === "error") {
|
|
160
|
+
return new Text(theme.fg("error", replaceTabs(errorMessage ?? "Unknown error")), 1, 0);
|
|
95
161
|
}
|
|
96
|
-
const text = replaceTabs(
|
|
162
|
+
const text = replaceTabs(answer).trim();
|
|
97
163
|
if (!text) {
|
|
98
|
-
const waiting =
|
|
99
|
-
this.#state === "running" ? `${theme.status.pending} Waiting for response…` : "No text returned.";
|
|
164
|
+
const waiting = state === "running" ? `${theme.status.pending} Waiting for response…` : "No text returned.";
|
|
100
165
|
return new Text(theme.fg("dim", waiting), 1, 0);
|
|
101
166
|
}
|
|
167
|
+
if (state === "running") return new Text(text, 1, 0);
|
|
102
168
|
return new Markdown(text, 1, 0, getMarkdownTheme());
|
|
103
169
|
}
|
|
104
170
|
}
|
|
@@ -35,6 +35,7 @@ import type { Settings } from "../../config/settings";
|
|
|
35
35
|
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
|
36
36
|
import { formatModelOnboardingInlineHint } from "../../setup/model-onboarding-guidance";
|
|
37
37
|
import { formatClampedModelSelector, getThinkingLevelMetadata, parseThinkingLevel } from "../../thinking";
|
|
38
|
+
import { getConfiguredImageModel } from "../../tools/image-gen";
|
|
38
39
|
import { getTabBarTheme } from "../shared";
|
|
39
40
|
import { DynamicBorder } from "./dynamic-border";
|
|
40
41
|
|
|
@@ -118,6 +119,9 @@ export type ModelSelectorSelection =
|
|
|
118
119
|
| {
|
|
119
120
|
kind: "deleteProfile";
|
|
120
121
|
profileName: string;
|
|
122
|
+
}
|
|
123
|
+
| {
|
|
124
|
+
kind: "imageGeneration";
|
|
121
125
|
};
|
|
122
126
|
|
|
123
127
|
interface PendingThinkingChoice {
|
|
@@ -183,13 +187,18 @@ interface PresetBrowseRow {
|
|
|
183
187
|
kind: "browse";
|
|
184
188
|
}
|
|
185
189
|
|
|
190
|
+
interface PresetImageGenerationRow {
|
|
191
|
+
kind: "imageGeneration";
|
|
192
|
+
}
|
|
193
|
+
|
|
186
194
|
type PresetLandingRow =
|
|
187
195
|
| PresetGroupRow
|
|
188
196
|
| PresetProfileRow
|
|
189
197
|
| PresetCreateRow
|
|
190
198
|
| PresetCreateUnavailableRow
|
|
191
199
|
| PresetAlreadySavedRow
|
|
192
|
-
| PresetBrowseRow
|
|
200
|
+
| PresetBrowseRow
|
|
201
|
+
| PresetImageGenerationRow;
|
|
193
202
|
|
|
194
203
|
// Stable logical identity for a preset landing row, independent of its current
|
|
195
204
|
// list position. Used to relocate the cursor after the expanded group changes so
|
|
@@ -208,6 +217,8 @@ function presetRowIdentity(row: PresetLandingRow): string {
|
|
|
208
217
|
return "createUnavailable";
|
|
209
218
|
case "alreadySaved":
|
|
210
219
|
return `alreadySaved:${row.profile.name}`;
|
|
220
|
+
case "imageGeneration":
|
|
221
|
+
return "imageGeneration";
|
|
211
222
|
}
|
|
212
223
|
}
|
|
213
224
|
|
|
@@ -962,6 +973,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
962
973
|
} else {
|
|
963
974
|
rows.push({ kind: "createUnavailable", label: "Select a model before creating a custom preset" });
|
|
964
975
|
}
|
|
976
|
+
rows.push({ kind: "imageGeneration" });
|
|
965
977
|
rows.push({ kind: "browse" });
|
|
966
978
|
return rows;
|
|
967
979
|
}
|
|
@@ -1047,7 +1059,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
1047
1059
|
selected.kind === "browse" ||
|
|
1048
1060
|
selected.kind === "create" ||
|
|
1049
1061
|
selected.kind === "createUnavailable" ||
|
|
1050
|
-
selected.kind === "alreadySaved"
|
|
1062
|
+
selected.kind === "alreadySaved" ||
|
|
1063
|
+
selected.kind === "imageGeneration"
|
|
1051
1064
|
)
|
|
1052
1065
|
return;
|
|
1053
1066
|
if (this.#expandedPresetProviderId === selected.groupId) return;
|
|
@@ -1063,7 +1076,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
1063
1076
|
selected.kind === "browse" ||
|
|
1064
1077
|
selected.kind === "create" ||
|
|
1065
1078
|
selected.kind === "createUnavailable" ||
|
|
1066
|
-
selected.kind === "alreadySaved"
|
|
1079
|
+
selected.kind === "alreadySaved" ||
|
|
1080
|
+
selected.kind === "imageGeneration"
|
|
1067
1081
|
)
|
|
1068
1082
|
return;
|
|
1069
1083
|
if (this.#expandedPresetProviderId !== selected.groupId) return;
|
|
@@ -1162,6 +1176,16 @@ export class ModelSelectorComponent extends Container {
|
|
|
1162
1176
|
this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
1163
1177
|
continue;
|
|
1164
1178
|
}
|
|
1179
|
+
if (row.kind === "imageGeneration") {
|
|
1180
|
+
const config = getConfiguredImageModel();
|
|
1181
|
+
const current =
|
|
1182
|
+
config && config.provider !== "auto"
|
|
1183
|
+
? `${config.provider}${config.model ? ` (${config.model})` : ""}`
|
|
1184
|
+
: "Auto";
|
|
1185
|
+
const label = `Image Generation: ${current}`;
|
|
1186
|
+
this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1165
1189
|
if (row.kind === "group") {
|
|
1166
1190
|
const authenticated = this.#isPresetGroupUsable(row.profiles);
|
|
1167
1191
|
const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
|
|
@@ -1646,6 +1670,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
1646
1670
|
this.#switchToModelMode();
|
|
1647
1671
|
return;
|
|
1648
1672
|
}
|
|
1673
|
+
if (row.kind === "imageGeneration") {
|
|
1674
|
+
this.#onSelectCallback({ kind: "imageGeneration" });
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1649
1677
|
if (row.kind === "group") {
|
|
1650
1678
|
// A group is a list of alternative presets; only surface a login hint
|
|
1651
1679
|
// when none of its members are usable. A partially-usable group stays
|
|
@@ -46,6 +46,7 @@ export interface StatusLineSettings {
|
|
|
46
46
|
previewHighlightSegment?: StatusLineSegmentId;
|
|
47
47
|
showHookStatus?: boolean;
|
|
48
48
|
showSkillHud?: boolean;
|
|
49
|
+
showActionHints?: boolean;
|
|
49
50
|
sessionAccent?: boolean;
|
|
50
51
|
maxRows?: number;
|
|
51
52
|
}
|
|
@@ -199,6 +200,7 @@ export class StatusLineComponent implements Component {
|
|
|
199
200
|
separator: settings.get("statusLine.separator"),
|
|
200
201
|
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
201
202
|
showSkillHud: settings.get("statusLine.showSkillHud"),
|
|
203
|
+
showActionHints: settings.get("statusLine.showActionHints"),
|
|
202
204
|
segmentOptions: settings.getGroup("statusLine").segmentOptions,
|
|
203
205
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
204
206
|
maxRows: settings.get("statusLine.maxRows"),
|
|
@@ -791,7 +793,10 @@ export class StatusLineComponent implements Component {
|
|
|
791
793
|
}
|
|
792
794
|
|
|
793
795
|
const right: string[] = [];
|
|
794
|
-
const actionHints =
|
|
796
|
+
const actionHints =
|
|
797
|
+
effectiveSettings.showActionHints === false
|
|
798
|
+
? []
|
|
799
|
+
: getAvailableActionHints(this.#actionRegistry, this.#getKeybindings, width, this.#focusDomain);
|
|
795
800
|
for (const segId of effectiveSettings.rightSegments) {
|
|
796
801
|
const rendered = renderSegment(segId, ctx);
|
|
797
802
|
if (rendered.visible && rendered.content) {
|