@gajae-code/coding-agent 0.6.3 → 0.6.4
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 +28 -0
- package/README.md +73 -1
- package/dist/types/config/settings-schema.d.ts +27 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/setup-cli.ts +14 -1
- package/src/commands/launch.ts +1 -1
- package/src/config/model-registry.ts +9 -2
- package/src/config/model-resolver.ts +13 -2
- package/src/config/settings-schema.ts +17 -0
- package/src/exec/bash-executor.ts +3 -1
- package/src/gjc-runtime/launch-tmux.ts +62 -14
- package/src/gjc-runtime/tmux-sessions.ts +36 -1
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/modes/components/welcome.ts +42 -9
- package/src/modes/controllers/input-controller.ts +21 -3
- package/src/modes/interactive-mode.ts +22 -1
- package/src/modes/prompt-action-autocomplete.ts +11 -1
- package/src/session/session-manager.ts +19 -2
- package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
- package/src/slash-commands/builtin-registry.ts +8 -4
- package/src/system-prompt.ts +11 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.4] - 2026-06-20
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Added `startup.welcomeBannerMode = "square"` for a square-corner Unicode welcome-logo fallback, and stopped treating Windows Terminal (`WT_SESSION`) as an automatic ASCII downgrade; `auto` now preserves the rounded Unicode logo while `unicode`, `square`, and `ascii` remain explicit overrides.
|
|
10
|
+
|
|
11
|
+
- Improved image input discoverability by adding an interactive `#paste-image` prompt action and clearer clipboard fallback guidance when no image is available.
|
|
12
|
+
|
|
13
|
+
- Improved skill migration guidance for users moving custom skills onto the current skill system (#899).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed native Windows tmux launch and hardened Windows tmux root launch, and resolved follow-up Windows tmux launch and input regressions (#884, #895, #906).
|
|
18
|
+
- Fixed `EXDEV` failures when moving session artifacts across filesystems (cross-device session artifact moves) (#886).
|
|
19
|
+
- Excluded user context files from the project prompt so file-level context filtering no longer leaks user-scoped files into project context (#885).
|
|
20
|
+
- Fixed a bash cancellation descendant-cleanup race so cancellation now waits for child-process cleanup within a bounded stall prompt (#893).
|
|
21
|
+
- Fixed the TUI dropping the first `/goal set <objective>` command from input history: the typed command is now recorded whenever args are supplied, regardless of prior goal-mode state (#910).
|
|
22
|
+
- Fixed Ctrl+Enter/Ctrl+Shift+Enter newline handling in the editor: idle Ctrl+Enter now falls through to newline insertion while keeping Ctrl+Enter as the busy-session follow-up shortcut, and Ctrl+Shift+Enter inserts a newline (#911).
|
|
23
|
+
- Fixed parsing of psmux modified-enter key sequences in the TUI (#918).
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
|
|
27
|
+
- Documented Windows Terminal welcome-logo troubleshooting with Cascadia Mono / Cascadia Mono Nerd Font and the profile `fontFace` setting.
|
|
28
|
+
- Documented CLI `@image` attachments and interactive TUI clipboard image paste fallbacks in the root README.
|
|
29
|
+
|
|
30
|
+
- Documented lifecycle notification hooks (#903).
|
|
31
|
+
- Added a routed GJC session guide for Clawhip/Hermes/OpenClaw visible routed sessions and linked it from the Hermes docs and operator instructions.
|
|
32
|
+
|
|
5
33
|
## [0.6.3] - 2026-06-19
|
|
6
34
|
|
|
7
35
|
### Fixed
|
package/README.md
CHANGED
|
@@ -11,9 +11,69 @@ Package-specific references:
|
|
|
11
11
|
- [DEVELOPMENT](./DEVELOPMENT.md)
|
|
12
12
|
- [RenderMermaid guide](../../docs/render-mermaid.md)
|
|
13
13
|
|
|
14
|
+
## External lifecycle notifications
|
|
15
|
+
|
|
16
|
+
GJC already exposes public lifecycle events through the extension/hook event contract. External notification integrations for Discord, Hermes, clawhip, or similar channels should be opt-in and subscribe to these events instead of scraping transcripts or logs:
|
|
17
|
+
|
|
18
|
+
- `turn_end` — a model/tool turn finished. The public payload is `{ type: "turn_end", turnIndex, message, toolResults }`.
|
|
19
|
+
- `agent_end` — the agent loop for a submitted prompt reached a terminal boundary. The public payload is `{ type: "agent_end", messages }`.
|
|
20
|
+
|
|
21
|
+
Recommended external mapping:
|
|
22
|
+
|
|
23
|
+
| Notification | Public event | Status guidance |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| Turn finished | `turn_end` | Use the handler's own sanitized status such as `"finished"`. |
|
|
26
|
+
| Agent stopped/finished | `agent_end` | Treat as terminal for the prompt. |
|
|
27
|
+
| Waiting/blocked/failed | `agent_end` plus a caller-supplied safe summary | Current lifecycle events do not expose a separate structured waiting/blocked reason; inspect only public-safe, integration-owned state. |
|
|
28
|
+
|
|
29
|
+
Forward only a minimal, caller-sanitized payload. Do not include raw prompts, assistant transcripts, hidden prompts, tool outputs, raw logs, host paths, private config, webhook URLs, channel IDs, tokens, or secrets. A safe notification payload should be built by the extension/hook itself, for example:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import type { ExtensionAPI } from "@gajae-code/coding-agent";
|
|
33
|
+
|
|
34
|
+
type PublicLifecycleNotification = {
|
|
35
|
+
type: "turn_end" | "agent_end";
|
|
36
|
+
status: "finished" | "stopped" | "failed" | "blocked" | "waiting";
|
|
37
|
+
turnIndex?: number;
|
|
38
|
+
timestamp: string;
|
|
39
|
+
summary: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default function lifecycleNotifier(pi: ExtensionAPI) {
|
|
43
|
+
const enabled = process.env.GJC_LIFECYCLE_NOTIFY === "1";
|
|
44
|
+
if (!enabled) return;
|
|
45
|
+
|
|
46
|
+
const send = async (payload: PublicLifecycleNotification) => {
|
|
47
|
+
// POST to Discord/Hermes/clawhip here. Keep target URLs and channel IDs in
|
|
48
|
+
// private config or environment variables; never include them in payloads.
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
pi.on("turn_end", event =>
|
|
52
|
+
send({
|
|
53
|
+
type: "turn_end",
|
|
54
|
+
status: "finished",
|
|
55
|
+
turnIndex: event.turnIndex,
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
summary: "GJC turn finished",
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
pi.on("agent_end", () =>
|
|
62
|
+
send({
|
|
63
|
+
type: "agent_end",
|
|
64
|
+
status: "stopped",
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
summary: "GJC prompt reached a terminal lifecycle boundary",
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This is the supported repo-native lifecycle notification path. It is not Claude Code hook compatibility, and it remains disabled unless the user configures an extension/hook handler and private delivery target.
|
|
73
|
+
|
|
14
74
|
## Memory backends
|
|
15
75
|
|
|
16
|
-
The agent supports three mutually-exclusive memory backends, selected via the `memory.backend` setting (Settings → Memory tab, or `~/.gjc/config.yml`):
|
|
76
|
+
The agent supports three mutually-exclusive memory backends, selected via the `memory.backend` setting (Settings → Memory tab, or `~/.gjc/agent/config.yml`):
|
|
17
77
|
|
|
18
78
|
- `off` (default) — no memory subsystem runs.
|
|
19
79
|
- `local` — existing rollout-summarisation pipeline; writes `memory_summary.md` and consolidated artifacts under the agent dir.
|
|
@@ -35,3 +95,15 @@ Switching backends mid-session is honoured on the next system-prompt rebuild and
|
|
|
35
95
|
## Red-claw TUI theme
|
|
36
96
|
|
|
37
97
|
The interactive TUI defaults to the bundled `red-claw` crustacean theme for dark terminals and the bundled `blue-crab` theme for light-appearance terminals, with matching welcome/icon assets. Three additional bundled migration themes — `claude-code`, `codex`, and `opencode` — mirror the look of those tools for easy eye-migration and are selectable from Settings or `/theme`. Explicit user theme settings still win; set `theme.dark: red-claw` and `theme.light: blue-crab` in `~/.gjc/agent/config.yml` to pin them.
|
|
98
|
+
|
|
99
|
+
### Welcome banner fonts on Windows Terminal
|
|
100
|
+
|
|
101
|
+
The startup logo defaults to rounded Unicode box drawing. Windows Terminal can render it correctly when the selected profile font has the needed box-drawing glyphs; recommended choices are `Cascadia Mono` or `Cascadia Mono Nerd Font`. In Windows Terminal Settings, set the profile font face to one of those fonts, or add it to the profile JSON:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
"font": {
|
|
105
|
+
"face": "Cascadia Mono"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
For terminals or fonts with broken rounded corners, set `startup.welcomeBannerMode` in `~/.gjc/agent/config.yml` to one of `unicode`, `square`, or `ascii`. `square` keeps a Unicode-looking logo using square corners (`┌ ┐ └ ┘`) while `ascii` uses only `+`, `-`, and `|`.
|
|
@@ -1195,6 +1195,33 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
1195
1195
|
readonly description: "Skip welcome screen and startup status messages";
|
|
1196
1196
|
};
|
|
1197
1197
|
};
|
|
1198
|
+
readonly "startup.welcomeBannerMode": {
|
|
1199
|
+
readonly type: "enum";
|
|
1200
|
+
readonly values: readonly ["auto", "unicode", "square", "ascii"];
|
|
1201
|
+
readonly default: "auto";
|
|
1202
|
+
readonly ui: {
|
|
1203
|
+
readonly tab: "interaction";
|
|
1204
|
+
readonly label: "Welcome Banner Mode";
|
|
1205
|
+
readonly description: "Logo style for the startup welcome screen";
|
|
1206
|
+
readonly options: readonly [{
|
|
1207
|
+
readonly value: "auto";
|
|
1208
|
+
readonly label: "Auto";
|
|
1209
|
+
readonly description: "Use the rounded Unicode logo";
|
|
1210
|
+
}, {
|
|
1211
|
+
readonly value: "unicode";
|
|
1212
|
+
readonly label: "Unicode";
|
|
1213
|
+
readonly description: "Force the rounded Unicode logo";
|
|
1214
|
+
}, {
|
|
1215
|
+
readonly value: "square";
|
|
1216
|
+
readonly label: "Square Unicode";
|
|
1217
|
+
readonly description: "Force the square-corner Unicode fallback";
|
|
1218
|
+
}, {
|
|
1219
|
+
readonly value: "ascii";
|
|
1220
|
+
readonly label: "ASCII";
|
|
1221
|
+
readonly description: "Force the ASCII-safe logo";
|
|
1222
|
+
}];
|
|
1223
|
+
};
|
|
1224
|
+
};
|
|
1198
1225
|
readonly "startup.checkUpdate": {
|
|
1199
1226
|
readonly type: "boolean";
|
|
1200
1227
|
readonly default: true;
|
|
@@ -32,6 +32,8 @@ export declare function listGjcTmuxSessions(env?: NodeJS.ProcessEnv): GjcTmuxSes
|
|
|
32
32
|
/** @internal */
|
|
33
33
|
export declare function listTmuxSessionsForGc(env?: NodeJS.ProcessEnv): GjcTmuxSessionsForGc;
|
|
34
34
|
export declare function findGjcTmuxSessionByBranch(branch: string, env?: NodeJS.ProcessEnv, project?: string | null): GjcTmuxSessionStatus | undefined;
|
|
35
|
+
export declare function findGjcTmuxSessionByName(sessionName: string, env?: NodeJS.ProcessEnv): GjcTmuxSessionStatus | undefined;
|
|
36
|
+
export declare function findGjcTmuxSessionByScope(project: string, branch: string | null | undefined, env?: NodeJS.ProcessEnv): GjcTmuxSessionStatus | undefined;
|
|
35
37
|
export declare function statusGjcTmuxSession(sessionName: string, env?: NodeJS.ProcessEnv): GjcTmuxSessionStatus;
|
|
36
38
|
export declare function createGjcTmuxSession(env?: NodeJS.ProcessEnv): GjcTmuxSessionStatus;
|
|
37
39
|
/** @internal */
|
|
@@ -8,6 +8,7 @@ export interface LspServerInfo {
|
|
|
8
8
|
status: "ready" | "error" | "connecting";
|
|
9
9
|
fileTypes: string[];
|
|
10
10
|
}
|
|
11
|
+
export type WelcomeLogoMode = "unicode" | "square" | "ascii";
|
|
11
12
|
/**
|
|
12
13
|
* GJC-native launch surface with compact command affordances, project
|
|
13
14
|
* signals, and a claw/talon mark without copying another agent shell.
|
|
@@ -19,7 +20,8 @@ export declare class WelcomeComponent implements Component {
|
|
|
19
20
|
private providerName;
|
|
20
21
|
private recentSessions;
|
|
21
22
|
private lspServers;
|
|
22
|
-
|
|
23
|
+
private readonly logoMode;
|
|
24
|
+
constructor(version: string, modelName: string, providerName: string, recentSessions?: RecentSession[], lspServers?: LspServerInfo[], logoMode?: WelcomeLogoMode);
|
|
23
25
|
invalidate(): void;
|
|
24
26
|
/**
|
|
25
27
|
* Play a one-shot intro that sweeps the gradient through every phase
|
|
@@ -23,9 +23,12 @@ import type { HookInputComponent } from "./components/hook-input";
|
|
|
23
23
|
import type { HookSelectorComponent } from "./components/hook-selector";
|
|
24
24
|
import { StatusLineComponent } from "./components/status-line";
|
|
25
25
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
26
|
+
import { type WelcomeLogoMode } from "./components/welcome";
|
|
26
27
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
27
28
|
import type { Theme } from "./theme/theme";
|
|
28
29
|
import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInput, TodoItem, TodoPhase } from "./types";
|
|
30
|
+
export type WelcomeBannerSettingMode = "auto" | "unicode" | "square" | "ascii";
|
|
31
|
+
export declare function resolveWelcomeLogoMode(mode: WelcomeBannerSettingMode, env?: Record<string, string | undefined>, platform?: NodeJS.Platform): WelcomeLogoMode;
|
|
29
32
|
/** Options for creating an InteractiveMode instance (for future API use) */
|
|
30
33
|
export interface InteractiveModeOptions {
|
|
31
34
|
/** Providers that were migrated during startup */
|
|
@@ -13,6 +13,7 @@ interface PromptActionAutocompleteOptions {
|
|
|
13
13
|
keybindings: KeybindingsManager;
|
|
14
14
|
copyCurrentLine: () => void;
|
|
15
15
|
copyPrompt: () => void;
|
|
16
|
+
pasteImage: () => void;
|
|
16
17
|
undo: (prefix: string) => void;
|
|
17
18
|
moveCursorToMessageEnd: () => void;
|
|
18
19
|
moveCursorToMessageStart: () => void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/coding-agent",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.4",
|
|
5
5
|
"description": "Gajae Code CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
52
52
|
"@babel/parser": "^7.29.3",
|
|
53
53
|
"@mozilla/readability": "^0.6.0",
|
|
54
|
-
"@gajae-code/stats": "0.6.
|
|
55
|
-
"@gajae-code/agent-core": "0.6.
|
|
56
|
-
"@gajae-code/ai": "0.6.
|
|
57
|
-
"@gajae-code/natives": "0.6.
|
|
58
|
-
"@gajae-code/tui": "0.6.
|
|
59
|
-
"@gajae-code/utils": "0.6.
|
|
54
|
+
"@gajae-code/stats": "0.6.4",
|
|
55
|
+
"@gajae-code/agent-core": "0.6.4",
|
|
56
|
+
"@gajae-code/ai": "0.6.4",
|
|
57
|
+
"@gajae-code/natives": "0.6.4",
|
|
58
|
+
"@gajae-code/tui": "0.6.4",
|
|
59
|
+
"@gajae-code/utils": "0.6.4",
|
|
60
60
|
"@puppeteer/browsers": "^2.13.0",
|
|
61
61
|
"@types/turndown": "5.0.6",
|
|
62
62
|
"@xterm/headless": "^6.0.0",
|
package/src/cli/setup-cli.ts
CHANGED
|
@@ -374,6 +374,7 @@ async function handleHooksSetup(flags: { json?: boolean; check?: boolean }): Pro
|
|
|
374
374
|
async function handleDefaultsSetup(flags: { json?: boolean; check?: boolean; force?: boolean }): Promise<void> {
|
|
375
375
|
const result = await installDefaultGjcDefinitions({ check: flags.check, force: flags.force });
|
|
376
376
|
const hasCheckFailure = result.missing > 0 || result.different > 0;
|
|
377
|
+
const inspectGuidance = `Inspect bundled skills with: ${APP_NAME} skills list; read one with: ${APP_NAME} skills read ralplan`;
|
|
377
378
|
|
|
378
379
|
if (flags.json) {
|
|
379
380
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -388,18 +389,30 @@ async function handleDefaultsSetup(flags: { json?: boolean; check?: boolean; for
|
|
|
388
389
|
console.error(
|
|
389
390
|
chalk.dim(`Missing: ${result.missing}; different: ${result.different}; matching: ${result.matching}`),
|
|
390
391
|
);
|
|
392
|
+
console.error(chalk.dim(inspectGuidance));
|
|
393
|
+
console.error(
|
|
394
|
+
chalk.dim(
|
|
395
|
+
`Compare embedded defaults before overwriting local files with: ${APP_NAME} setup defaults --force`,
|
|
396
|
+
),
|
|
397
|
+
);
|
|
391
398
|
process.exit(1);
|
|
392
399
|
}
|
|
393
400
|
console.log(chalk.green(`${theme.status.success} Default GJC workflow skills are installed`));
|
|
394
401
|
console.log(chalk.dim(`Target: ${result.targetRoot}`));
|
|
402
|
+
console.log(chalk.dim(inspectGuidance));
|
|
395
403
|
return;
|
|
396
404
|
}
|
|
397
405
|
|
|
398
406
|
console.log(chalk.green(`${theme.status.success} Default GJC workflow skills installed`));
|
|
399
407
|
console.log(chalk.dim(`Target: ${result.targetRoot}`));
|
|
400
408
|
console.log(chalk.dim(`Written: ${result.written}; skipped: ${result.skipped}`));
|
|
409
|
+
console.log(chalk.dim(inspectGuidance));
|
|
401
410
|
if (result.skipped > 0 && !flags.force) {
|
|
402
|
-
console.log(
|
|
411
|
+
console.log(
|
|
412
|
+
chalk.dim(
|
|
413
|
+
`Existing local default workflow skill files were preserved. Use ${APP_NAME} setup defaults --force to overwrite them intentionally.`,
|
|
414
|
+
),
|
|
415
|
+
);
|
|
403
416
|
}
|
|
404
417
|
}
|
|
405
418
|
|
package/src/commands/launch.ts
CHANGED
|
@@ -169,7 +169,7 @@ export default class Index extends Command {
|
|
|
169
169
|
rawArgs: launch.args,
|
|
170
170
|
cwd: launch.cwd,
|
|
171
171
|
worktreeBranch: launch.worktree.enabled && !launch.worktree.detached ? launch.worktree.branchName : null,
|
|
172
|
-
project: launch.
|
|
172
|
+
project: launch.cwd,
|
|
173
173
|
})
|
|
174
174
|
)
|
|
175
175
|
return;
|
|
@@ -872,7 +872,13 @@ const customReferenceMap = buildCustomReferenceMap();
|
|
|
872
872
|
|
|
873
873
|
function getCustomReferenceCandidateIds(modelId: string): string[] {
|
|
874
874
|
const candidates = new Set<string>();
|
|
875
|
-
const
|
|
875
|
+
const minimaxM = /^minimax-m(\d+(?:\.\d+)*)$/i.exec(modelId.trim());
|
|
876
|
+
const queue = minimaxM ? [`MiniMax-M${minimaxM[1]}`, modelId] : [modelId];
|
|
877
|
+
if (minimaxM) {
|
|
878
|
+
// MiniMax catalogs include lowercase wire ids plus display-cased aliases.
|
|
879
|
+
// Custom providers should keep the lowercase wire id while inheriting the
|
|
880
|
+
// canonical display casing when the alias exists.
|
|
881
|
+
}
|
|
876
882
|
for (let index = 0; index < queue.length; index += 1) {
|
|
877
883
|
const candidate = queue[index]?.trim();
|
|
878
884
|
if (!candidate || candidates.has(candidate)) continue;
|
|
@@ -1212,13 +1218,14 @@ export class ModelRegistry {
|
|
|
1212
1218
|
const existingIndex = indexByKey.get(key);
|
|
1213
1219
|
if (existingIndex !== undefined) {
|
|
1214
1220
|
const existingModel = merged[existingIndex];
|
|
1221
|
+
const referenceModel = resolveCustomModelReference(customModel.id);
|
|
1215
1222
|
merged[existingIndex] = enrichModelThinking({
|
|
1216
1223
|
...existingModel,
|
|
1217
1224
|
id: customModel.id,
|
|
1218
1225
|
provider: customModel.provider,
|
|
1219
1226
|
api: customModel.api,
|
|
1220
1227
|
baseUrl: customModel.baseUrl,
|
|
1221
|
-
name: customModel.name ?? existingModel.name,
|
|
1228
|
+
name: customModel.name ?? referenceModel?.name ?? existingModel.name,
|
|
1222
1229
|
reasoning: customModel.reasoning ?? existingModel.reasoning,
|
|
1223
1230
|
thinking: customModel.thinking ?? existingModel.thinking,
|
|
1224
1231
|
input: customModel.input ?? existingModel.input,
|
|
@@ -147,12 +147,23 @@ export function resolveProviderModelReference(
|
|
|
147
147
|
modelId: string,
|
|
148
148
|
availableModels: readonly Model<Api>[],
|
|
149
149
|
): Model<Api> | undefined {
|
|
150
|
-
const
|
|
151
|
-
const
|
|
150
|
+
const trimmedProvider = provider.trim();
|
|
151
|
+
const trimmedModelId = modelId.trim();
|
|
152
|
+
const normalizedProvider = trimmedProvider.toLowerCase();
|
|
153
|
+
const normalizedModelId = trimmedModelId.toLowerCase();
|
|
152
154
|
if (!normalizedProvider || !normalizedModelId) {
|
|
153
155
|
return undefined;
|
|
154
156
|
}
|
|
155
157
|
|
|
158
|
+
// Prefer an exact provider/model id match before falling back to the
|
|
159
|
+
// case-insensitive index. Some provider catalogs intentionally carry both a
|
|
160
|
+
// machine-id form (for example `minimax-m3`) and a display-cased alias
|
|
161
|
+
// (`MiniMax-M3`). The lowercase index correctly treats those aliases as
|
|
162
|
+
// ambiguous for fuzzy/case-insensitive lookup, but an exact selector should
|
|
163
|
+
// still resolve deterministically.
|
|
164
|
+
const caseExact = availableModels.find(m => m.provider === trimmedProvider && m.id === trimmedModelId);
|
|
165
|
+
if (caseExact) return caseExact;
|
|
166
|
+
|
|
156
167
|
const index = getProviderModelIndex(availableModels);
|
|
157
168
|
const exact = index.get(`${normalizedProvider}\u0000${normalizedModelId}`);
|
|
158
169
|
if (exact === null) {
|
|
@@ -1065,6 +1065,23 @@ export const SETTINGS_SCHEMA = {
|
|
|
1065
1065
|
},
|
|
1066
1066
|
},
|
|
1067
1067
|
|
|
1068
|
+
"startup.welcomeBannerMode": {
|
|
1069
|
+
type: "enum",
|
|
1070
|
+
values: ["auto", "unicode", "square", "ascii"] as const,
|
|
1071
|
+
default: "auto",
|
|
1072
|
+
ui: {
|
|
1073
|
+
tab: "interaction",
|
|
1074
|
+
label: "Welcome Banner Mode",
|
|
1075
|
+
description: "Logo style for the startup welcome screen",
|
|
1076
|
+
options: [
|
|
1077
|
+
{ value: "auto", label: "Auto", description: "Use the rounded Unicode logo" },
|
|
1078
|
+
{ value: "unicode", label: "Unicode", description: "Force the rounded Unicode logo" },
|
|
1079
|
+
{ value: "square", label: "Square Unicode", description: "Force the square-corner Unicode fallback" },
|
|
1080
|
+
{ value: "ascii", label: "ASCII", description: "Force the ASCII-safe logo" },
|
|
1081
|
+
],
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
|
|
1068
1085
|
"startup.checkUpdate": {
|
|
1069
1086
|
type: "boolean",
|
|
1070
1087
|
default: true,
|
|
@@ -66,7 +66,9 @@ export interface BashResult {
|
|
|
66
66
|
const shellSessions = new Map<string, Shell>();
|
|
67
67
|
const brokenShellSessions = new Set<string>();
|
|
68
68
|
const retiringShellSessions = new Set<Shell>();
|
|
69
|
-
|
|
69
|
+
// Cover pi-shell's normal cancellation kill waves without turning a stalled
|
|
70
|
+
// native cleanup into a multi-second JavaScript tool stall.
|
|
71
|
+
const CANCEL_CLEANUP_WAIT_MS = 400;
|
|
70
72
|
|
|
71
73
|
/** Number of persistent shell sessions currently retained (owner gauge). */
|
|
72
74
|
export function getShellSessionCount(): number {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { safeStderrWrite } from "@gajae-code/utils";
|
|
3
4
|
import type { Args } from "../cli/args";
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
type GjcTmuxProfileCommand,
|
|
15
16
|
resolveGjcTmuxCommand,
|
|
16
17
|
} from "./tmux-common";
|
|
17
|
-
import {
|
|
18
|
+
import { findGjcTmuxSessionByName, findGjcTmuxSessionByScope } from "./tmux-sessions";
|
|
18
19
|
|
|
19
20
|
export {
|
|
20
21
|
buildGjcTmuxProfileCommands,
|
|
@@ -83,6 +84,20 @@ export interface TmuxLaunchPlan {
|
|
|
83
84
|
sessionStateFile?: string | null;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
function explicitTmuxSessionName(env: NodeJS.ProcessEnv): string | undefined {
|
|
88
|
+
return env.GJC_TMUX_SESSION?.trim() || undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findExistingSessionForLaunch(context: {
|
|
92
|
+
env: NodeJS.ProcessEnv;
|
|
93
|
+
project: string;
|
|
94
|
+
branch?: string | null;
|
|
95
|
+
}): string | undefined {
|
|
96
|
+
const explicit = explicitTmuxSessionName(context.env);
|
|
97
|
+
if (explicit) return findGjcTmuxSessionByName(explicit, context.env)?.name;
|
|
98
|
+
return findGjcTmuxSessionByScope(context.project, context.branch, context.env)?.name;
|
|
99
|
+
}
|
|
100
|
+
|
|
86
101
|
export interface GjcTmuxProfileResult {
|
|
87
102
|
skipped: boolean;
|
|
88
103
|
commands: GjcTmuxProfileCommand[];
|
|
@@ -107,6 +122,7 @@ interface CommandResolutionContext {
|
|
|
107
122
|
argv: string[];
|
|
108
123
|
execPath: string;
|
|
109
124
|
extraEnv?: Record<string, string>;
|
|
125
|
+
platform?: NodeJS.Platform;
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
function parseLaunchPolicy(env: NodeJS.ProcessEnv): LaunchPolicy {
|
|
@@ -148,6 +164,26 @@ function buildEnvAssignments(values: Record<string, string> | undefined): string
|
|
|
148
164
|
const entries = Object.entries(values ?? {});
|
|
149
165
|
return entries.length === 0 ? "" : ` ${entries.map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ")}`;
|
|
150
166
|
}
|
|
167
|
+
function powershellQuote(value: string): string {
|
|
168
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
169
|
+
}
|
|
170
|
+
function stripRootTmuxFlag(rawArgs: string[]): string[] {
|
|
171
|
+
return rawArgs.filter(arg => arg !== "--tmux");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
|
|
175
|
+
const command = resolveCurrentGjcCommand(context);
|
|
176
|
+
const envLines = Object.entries({ [GJC_TMUX_LAUNCHED_ENV]: "1", ...(context.extraEnv ?? {}) }).map(
|
|
177
|
+
([key, value]) => `$env:${key} = ${powershellQuote(value)}`,
|
|
178
|
+
);
|
|
179
|
+
const invocation = ["&", ...command.map(powershellQuote), ...stripRootTmuxFlag(rawArgs).map(powershellQuote)].join(
|
|
180
|
+
" ",
|
|
181
|
+
);
|
|
182
|
+
const exitLine = "if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }";
|
|
183
|
+
const script = [...envLines, invocation, exitLine].join("\n");
|
|
184
|
+
const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
|
|
185
|
+
return `pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`;
|
|
186
|
+
}
|
|
151
187
|
|
|
152
188
|
export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProfileResult {
|
|
153
189
|
const env = context.env ?? process.env;
|
|
@@ -177,16 +213,26 @@ function resolveCurrentGjcCommand(context: CommandResolutionContext): string[] {
|
|
|
177
213
|
if (isBunVirtualPath(entrypoint)) {
|
|
178
214
|
return isBunVirtualPath(context.execPath) ? ["gjc"] : [context.execPath];
|
|
179
215
|
}
|
|
180
|
-
const
|
|
216
|
+
const pathModule = pathModuleForPlatform(context.platform);
|
|
217
|
+
const resolvedEntrypoint = pathModule.isAbsolute(entrypoint)
|
|
218
|
+
? entrypoint
|
|
219
|
+
: pathModule.resolve(context.cwd, entrypoint);
|
|
181
220
|
if (entrypoint.endsWith(".ts") || entrypoint.endsWith(".js") || entrypoint.endsWith(".mjs")) {
|
|
182
221
|
return [context.execPath, resolvedEntrypoint];
|
|
183
222
|
}
|
|
184
223
|
return [resolvedEntrypoint];
|
|
185
224
|
}
|
|
225
|
+
function isWindowsPlatform(platform: NodeJS.Platform | undefined): boolean {
|
|
226
|
+
return platform === "win32";
|
|
227
|
+
}
|
|
228
|
+
function pathModuleForPlatform(platform: NodeJS.Platform | undefined): typeof path.win32 | typeof path {
|
|
229
|
+
return isWindowsPlatform(platform) ? path.win32 : path;
|
|
230
|
+
}
|
|
186
231
|
|
|
187
232
|
function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
|
|
233
|
+
if (isWindowsPlatform(context.platform)) return buildWindowsPowerShellInnerCommand(context, rawArgs);
|
|
188
234
|
const command = resolveCurrentGjcCommand(context);
|
|
189
|
-
const quoted = [...command, ...rawArgs].map(shellQuote).join(" ");
|
|
235
|
+
const quoted = [...command, ...stripRootTmuxFlag(rawArgs)].map(shellQuote).join(" ");
|
|
190
236
|
return `exec env ${GJC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments(context.extraEnv)} ${quoted}`;
|
|
191
237
|
}
|
|
192
238
|
|
|
@@ -305,7 +351,6 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
305
351
|
if (!context.parsed.tmux || policy === "direct") return undefined;
|
|
306
352
|
if (env.TMUX || env[GJC_TMUX_LAUNCHED_ENV] === "1") return undefined;
|
|
307
353
|
const platform = context.platform ?? process.platform;
|
|
308
|
-
if (platform === "win32") return undefined;
|
|
309
354
|
const tty = context.tty ?? { stdin: Boolean(process.stdin.isTTY), stdout: Boolean(process.stdout.isTTY) };
|
|
310
355
|
if (policy === "tmux" && !isInteractiveRootLaunch(context.parsed, tty)) return undefined;
|
|
311
356
|
|
|
@@ -320,12 +365,14 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
320
365
|
path.join(cwd, ".gjc", "runtime", "tmux-sessions", `${buildGjcTmuxSessionSlug(sessionName)}.json`);
|
|
321
366
|
const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
|
|
322
367
|
if (!tmuxAvailable) return undefined;
|
|
323
|
-
const
|
|
368
|
+
const existingSessionName =
|
|
324
369
|
"existingBranchSessionName" in context
|
|
325
370
|
? (context.existingBranchSessionName ?? undefined)
|
|
326
|
-
:
|
|
327
|
-
|
|
328
|
-
|
|
371
|
+
: findExistingSessionForLaunch({
|
|
372
|
+
env,
|
|
373
|
+
project,
|
|
374
|
+
branch,
|
|
375
|
+
});
|
|
329
376
|
const innerCommand = buildInnerCommand(
|
|
330
377
|
{
|
|
331
378
|
cwd,
|
|
@@ -335,6 +382,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
335
382
|
[GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
|
|
336
383
|
[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
|
|
337
384
|
},
|
|
385
|
+
platform,
|
|
338
386
|
},
|
|
339
387
|
context.rawArgs,
|
|
340
388
|
);
|
|
@@ -348,7 +396,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
348
396
|
project,
|
|
349
397
|
sessionId,
|
|
350
398
|
sessionStateFile,
|
|
351
|
-
attachSessionName:
|
|
399
|
+
attachSessionName: existingSessionName,
|
|
352
400
|
};
|
|
353
401
|
}
|
|
354
402
|
|
|
@@ -405,19 +453,19 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
405
453
|
sessionId: plan.sessionId ?? null,
|
|
406
454
|
sessionStateFile: plan.sessionStateFile ?? null,
|
|
407
455
|
});
|
|
408
|
-
|
|
456
|
+
const ownershipFailure = profile.failures.find(item => item.command.args.includes("@gjc-profile"));
|
|
457
|
+
if (ownershipFailure) {
|
|
409
458
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
410
|
-
const failure =
|
|
411
|
-
profile.failures.find(item => item.command.args.includes("@gjc-profile")) ?? profile.failures[0];
|
|
412
459
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
413
|
-
formatTmuxLaunchDiagnostic("profile tagging failed",
|
|
460
|
+
formatTmuxLaunchDiagnostic("profile tagging failed", ownershipFailure.stderr),
|
|
414
461
|
);
|
|
415
462
|
return true;
|
|
416
463
|
}
|
|
417
464
|
}
|
|
418
465
|
if (created.exitCode !== 0) return false;
|
|
419
|
-
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
|
|
466
|
+
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.sessionName}`], options);
|
|
420
467
|
if (attached.exitCode === 0) return true;
|
|
468
|
+
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
421
469
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
422
470
|
return true;
|
|
423
471
|
}
|
|
@@ -137,6 +137,8 @@ function listRawTmuxSessionNames(env: NodeJS.ProcessEnv = process.env): string[]
|
|
|
137
137
|
export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus[] {
|
|
138
138
|
return listSessionLines(env)
|
|
139
139
|
.map(parseSessionLine)
|
|
140
|
+
.filter((session): session is GjcTmuxSessionStatus => session != null)
|
|
141
|
+
.map(session => hydrateSessionFromExactOptions(session, env))
|
|
140
142
|
.filter((session): session is GjcTmuxSessionStatus => session?.profile === GJC_TMUX_PROFILE_VALUE)
|
|
141
143
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
142
144
|
}
|
|
@@ -145,7 +147,8 @@ export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTm
|
|
|
145
147
|
export function listTmuxSessionsForGc(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionsForGc {
|
|
146
148
|
const sessions = listSessionLines(env)
|
|
147
149
|
.map(parseSessionLine)
|
|
148
|
-
.filter((session): session is GjcTmuxSessionStatus => session != null)
|
|
150
|
+
.filter((session): session is GjcTmuxSessionStatus => session != null)
|
|
151
|
+
.map(session => hydrateSessionFromExactOptions(session, env));
|
|
149
152
|
const tagged = sessions
|
|
150
153
|
.filter(session => session.profile === GJC_TMUX_PROFILE_VALUE)
|
|
151
154
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -179,6 +182,22 @@ export function findGjcTmuxSessionByBranch(
|
|
|
179
182
|
);
|
|
180
183
|
}
|
|
181
184
|
|
|
185
|
+
export function findGjcTmuxSessionByName(
|
|
186
|
+
sessionName: string,
|
|
187
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
188
|
+
): GjcTmuxSessionStatus | undefined {
|
|
189
|
+
return listGjcTmuxSessions(env).find(session => session.name === sessionName);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function findGjcTmuxSessionByScope(
|
|
193
|
+
project: string,
|
|
194
|
+
branch: string | null | undefined,
|
|
195
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
196
|
+
): GjcTmuxSessionStatus | undefined {
|
|
197
|
+
return listGjcTmuxSessions(env).find(
|
|
198
|
+
session => session.project === project && (branch ? session.branch === branch : session.branch === undefined),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
182
201
|
export function statusGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus {
|
|
183
202
|
const session = listGjcTmuxSessions(env).find(candidate => candidate.name === sessionName);
|
|
184
203
|
if (session) return session;
|
|
@@ -227,6 +246,22 @@ function readExactOptionForGc(sessionName: string, option: string, env: NodeJS.P
|
|
|
227
246
|
}
|
|
228
247
|
}
|
|
229
248
|
|
|
249
|
+
function hydrateSessionFromExactOptions(session: GjcTmuxSessionStatus, env: NodeJS.ProcessEnv): GjcTmuxSessionStatus {
|
|
250
|
+
if (session.profile === GJC_TMUX_PROFILE_VALUE) return session;
|
|
251
|
+
const profile = readExactOptionForGc(session.name, GJC_TMUX_PROFILE_OPTION, env);
|
|
252
|
+
if (profile !== GJC_TMUX_PROFILE_VALUE) return session;
|
|
253
|
+
return {
|
|
254
|
+
...session,
|
|
255
|
+
profile,
|
|
256
|
+
branch: session.branch ?? readExactOptionForGc(session.name, GJC_TMUX_BRANCH_OPTION, env),
|
|
257
|
+
branchSlug: session.branchSlug ?? readExactOptionForGc(session.name, GJC_TMUX_BRANCH_SLUG_OPTION, env),
|
|
258
|
+
project: session.project ?? readExactOptionForGc(session.name, GJC_TMUX_PROJECT_OPTION, env),
|
|
259
|
+
sessionId: session.sessionId ?? readExactOptionForGc(session.name, GJC_TMUX_SESSION_ID_OPTION, env),
|
|
260
|
+
sessionStateFile:
|
|
261
|
+
session.sessionStateFile ?? readExactOptionForGc(session.name, GJC_TMUX_SESSION_STATE_FILE_OPTION, env),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
230
265
|
/** @internal */
|
|
231
266
|
export function readTmuxSessionTagsForGc(
|
|
232
267
|
sessionName: string,
|