@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
|
@@ -199,6 +199,9 @@ export class InputController {
|
|
|
199
199
|
this.ctx.editor.onDequeue = () => this.handleDequeue();
|
|
200
200
|
this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
|
|
201
201
|
this.ctx.editor.onQueue = () => void this.handleQueueSubmit();
|
|
202
|
+
this.ctx.editor.onTabDeclined = () => {
|
|
203
|
+
if (this.ctx.session.isStreaming) void this.handleQueueSubmit();
|
|
204
|
+
};
|
|
202
205
|
|
|
203
206
|
this.ctx.editor.clearCustomKeyHandlers();
|
|
204
207
|
// Wire up extension shortcuts
|
|
@@ -228,7 +231,11 @@ export class InputController {
|
|
|
228
231
|
});
|
|
229
232
|
}
|
|
230
233
|
for (const key of this.ctx.keybindings.getKeys("app.message.followUp")) {
|
|
231
|
-
this.ctx.editor.setCustomKeyHandler(key, () =>
|
|
234
|
+
this.ctx.editor.setCustomKeyHandler(key, () => {
|
|
235
|
+
if (!this.#isFollowUpShortcutActive()) return false;
|
|
236
|
+
void this.handleFollowUp();
|
|
237
|
+
return true;
|
|
238
|
+
});
|
|
232
239
|
}
|
|
233
240
|
for (const key of this.ctx.keybindings.getKeys("app.stt.toggle")) {
|
|
234
241
|
this.ctx.editor.setCustomKeyHandler(key, () => void this.ctx.handleSTTToggle());
|
|
@@ -503,6 +510,15 @@ export class InputController {
|
|
|
503
510
|
return this.ctx.settings.get("busyPromptMode") === "steer" ? "steer" : "followUp";
|
|
504
511
|
}
|
|
505
512
|
|
|
513
|
+
#isFollowUpShortcutActive(): boolean {
|
|
514
|
+
return (
|
|
515
|
+
this.ctx.session.isStreaming ||
|
|
516
|
+
this.ctx.session.isCompacting ||
|
|
517
|
+
this.ctx.session.isBashRunning ||
|
|
518
|
+
this.ctx.session.isEvalRunning
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
506
522
|
/**
|
|
507
523
|
* Dispatch skill slash invocation(s) (`/skill:<name>`) through custom messages
|
|
508
524
|
* using the supplied `streamingBehavior`. Returns true if the text was a
|
|
@@ -824,8 +840,9 @@ export class InputController {
|
|
|
824
840
|
this.ctx.ui.requestRender();
|
|
825
841
|
return true;
|
|
826
842
|
}
|
|
827
|
-
|
|
828
|
-
|
|
843
|
+
this.ctx.showStatus(
|
|
844
|
+
"No image in clipboard. Use #paste-image, paste a copied image, or attach an image file with @path/to/image.png.",
|
|
845
|
+
);
|
|
829
846
|
return false;
|
|
830
847
|
} catch {
|
|
831
848
|
this.ctx.showStatus("Failed to read clipboard");
|
|
@@ -840,6 +857,7 @@ export class InputController {
|
|
|
840
857
|
keybindings: this.ctx.keybindings,
|
|
841
858
|
copyCurrentLine: () => this.handleCopyCurrentLine(),
|
|
842
859
|
copyPrompt: () => this.handleCopyPrompt(),
|
|
860
|
+
pasteImage: () => void this.handleImagePaste(),
|
|
843
861
|
undo: prefix => this.ctx.editor.undoPastTransientText(prefix),
|
|
844
862
|
moveCursorToMessageEnd: () => this.ctx.editor.moveToMessageEnd(),
|
|
845
863
|
moveCursorToMessageStart: () => this.ctx.editor.moveToMessageStart(),
|
|
@@ -86,7 +86,11 @@ import type { HookInputComponent } from "./components/hook-input";
|
|
|
86
86
|
import type { HookSelectorComponent } from "./components/hook-selector";
|
|
87
87
|
import { StatusLineComponent } from "./components/status-line";
|
|
88
88
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
89
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
WelcomeComponent,
|
|
91
|
+
type WelcomeLogoMode,
|
|
92
|
+
type LspServerInfo as WelcomeLspServerInfo,
|
|
93
|
+
} from "./components/welcome";
|
|
90
94
|
import { BtwController } from "./controllers/btw-controller";
|
|
91
95
|
import { CommandController } from "./controllers/command-controller";
|
|
92
96
|
import { EventController } from "./controllers/event-controller";
|
|
@@ -218,6 +222,21 @@ function parseGoalSubcommand(args: string): { sub: GoalSubcommand | undefined; r
|
|
|
218
222
|
return { sub: undefined, rest: trimmed };
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
export type WelcomeBannerSettingMode = "auto" | "unicode" | "square" | "ascii";
|
|
226
|
+
|
|
227
|
+
export function resolveWelcomeLogoMode(
|
|
228
|
+
mode: WelcomeBannerSettingMode,
|
|
229
|
+
env: Record<string, string | undefined> = Bun.env,
|
|
230
|
+
platform: NodeJS.Platform = process.platform,
|
|
231
|
+
): WelcomeLogoMode {
|
|
232
|
+
void env;
|
|
233
|
+
void platform;
|
|
234
|
+
if (mode === "unicode") return "unicode";
|
|
235
|
+
if (mode === "square") return "square";
|
|
236
|
+
if (mode === "ascii") return "ascii";
|
|
237
|
+
return "unicode";
|
|
238
|
+
}
|
|
239
|
+
|
|
221
240
|
/** Options for creating an InteractiveMode instance (for future API use) */
|
|
222
241
|
export interface InteractiveModeOptions {
|
|
223
242
|
/** Providers that were migrated during startup */
|
|
@@ -479,6 +498,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
479
498
|
);
|
|
480
499
|
|
|
481
500
|
const startupQuiet = settings.get("startup.quiet");
|
|
501
|
+
const welcomeLogoMode = resolveWelcomeLogoMode(settings.get("startup.welcomeBannerMode"));
|
|
482
502
|
this.#welcomeComponent = undefined;
|
|
483
503
|
|
|
484
504
|
for (const warning of this.session.configWarnings) {
|
|
@@ -494,6 +514,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
494
514
|
providerName,
|
|
495
515
|
recentSessions,
|
|
496
516
|
this.#getWelcomeLspServers(),
|
|
517
|
+
welcomeLogoMode,
|
|
497
518
|
);
|
|
498
519
|
|
|
499
520
|
// Setup UI layout
|
|
@@ -28,6 +28,7 @@ interface PromptActionAutocompleteOptions {
|
|
|
28
28
|
keybindings: KeybindingsManager;
|
|
29
29
|
copyCurrentLine: () => void;
|
|
30
30
|
copyPrompt: () => void;
|
|
31
|
+
pasteImage: () => void;
|
|
31
32
|
undo: (prefix: string) => void;
|
|
32
33
|
moveCursorToMessageEnd: () => void;
|
|
33
34
|
moveCursorToMessageStart: () => void;
|
|
@@ -190,7 +191,9 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
190
191
|
const query = promptActionPrefix.slice(1).toLowerCase();
|
|
191
192
|
const items = this.#actions
|
|
192
193
|
.map(action => {
|
|
193
|
-
const searchable = [action.label, action.description, ...action.keywords]
|
|
194
|
+
const searchable = [action.id, action.label, action.description, ...action.keywords]
|
|
195
|
+
.join(" ")
|
|
196
|
+
.toLowerCase();
|
|
194
197
|
if (!fuzzyMatch(query, searchable)) return null;
|
|
195
198
|
return {
|
|
196
199
|
value: action.label,
|
|
@@ -368,6 +371,13 @@ export function createPromptActionAutocompleteProvider(
|
|
|
368
371
|
keywords: ["copy", "prompt", "clipboard", "message"],
|
|
369
372
|
execute: options.copyPrompt,
|
|
370
373
|
},
|
|
374
|
+
{
|
|
375
|
+
id: "paste-image",
|
|
376
|
+
label: "Paste image from clipboard",
|
|
377
|
+
description: formatKeyHints(options.keybindings.getKeys("app.clipboard.pasteImage")),
|
|
378
|
+
keywords: ["paste", "image", "clipboard", "screenshot", "attach", "vision"],
|
|
379
|
+
execute: options.pasteImage,
|
|
380
|
+
},
|
|
371
381
|
{
|
|
372
382
|
id: "undo",
|
|
373
383
|
label: "Undo",
|
|
@@ -1129,6 +1129,23 @@ function formatTimeAgo(date: Date): string {
|
|
|
1129
1129
|
return date.toLocaleDateString();
|
|
1130
1130
|
}
|
|
1131
1131
|
|
|
1132
|
+
async function movePathAcrossDevicesSafe(source: string, destination: string): Promise<void> {
|
|
1133
|
+
try {
|
|
1134
|
+
await fs.promises.rename(source, destination);
|
|
1135
|
+
return;
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
if (!hasFsCode(error, "EXDEV")) throw error;
|
|
1138
|
+
}
|
|
1139
|
+
const stat = await fs.promises.stat(source);
|
|
1140
|
+
if (stat.isDirectory()) {
|
|
1141
|
+
await fs.promises.cp(source, destination, { recursive: true, force: false, errorOnExist: true });
|
|
1142
|
+
await fs.promises.rm(source, { recursive: true, force: false });
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_EXCL);
|
|
1146
|
+
await fs.promises.unlink(source);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1132
1149
|
const MAX_PERSIST_CHARS = 500_000;
|
|
1133
1150
|
const TRUNCATION_NOTICE = "\n\n[Session persistence truncated large content]";
|
|
1134
1151
|
/** Minimum base64 length to externalize to blob store (skip tiny inline images) */
|
|
@@ -2498,14 +2515,14 @@ export class SessionManager {
|
|
|
2498
2515
|
try {
|
|
2499
2516
|
// Guard: session file may not exist yet (no assistant messages persisted)
|
|
2500
2517
|
if (hadSessionFile) {
|
|
2501
|
-
await
|
|
2518
|
+
await movePathAcrossDevicesSafe(oldSessionFile, newSessionFile);
|
|
2502
2519
|
movedSessionFile = true;
|
|
2503
2520
|
}
|
|
2504
2521
|
|
|
2505
2522
|
try {
|
|
2506
2523
|
const stat = await fs.promises.stat(oldArtifactDir);
|
|
2507
2524
|
if (stat.isDirectory()) {
|
|
2508
|
-
await
|
|
2525
|
+
await movePathAcrossDevicesSafe(oldArtifactDir, newArtifactDir);
|
|
2509
2526
|
movedArtifactDir = true;
|
|
2510
2527
|
}
|
|
2511
2528
|
} catch (err) {
|
|
@@ -29,6 +29,14 @@ The Hermes bridge does not choose a model/provider. Generated setup configures `
|
|
|
29
29
|
|
|
30
30
|
Provider-specific commands are examples only, never product defaults.
|
|
31
31
|
|
|
32
|
+
## Visible routed-session fallback
|
|
33
|
+
|
|
34
|
+
If a Hermes/OpenClaw/Clawhip-style operator needs a human-visible, channel-routed GJC pane instead of a pure Coordinator MCP session, use the visible session pattern in [`docs/gjc-session-clawhip-routing.md`](../../../../../../docs/gjc-session-clawhip-routing.md).
|
|
35
|
+
|
|
36
|
+
Use that pattern only when the router must watch tmux output, send stale-session alerts, or inject follow-up prompts into the same visible pane. The short version is: prepare a dedicated worktree, register a stable tmux session through the host router, start interactive `gjc`, wait for TUI readiness, inject the task prompt separately, and verify actual tool/work activity before reporting acceptance.
|
|
37
|
+
|
|
38
|
+
Do not put private channel ids, mention targets, socket names, tokens, or local routing policy into portable setup output. Keep those in the host/operator deployment.
|
|
39
|
+
|
|
32
40
|
## Safety
|
|
33
41
|
|
|
34
42
|
- Mutating tools require bridge startup mutation classes and per-call consent.
|
|
@@ -250,11 +250,15 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
250
250
|
inlineHint: "[objective]",
|
|
251
251
|
allowArgs: true,
|
|
252
252
|
handleTui: async (command, runtime) => {
|
|
253
|
-
|
|
254
|
-
//
|
|
255
|
-
|
|
253
|
+
// The goal command always consumes the typed input: it either submits
|
|
254
|
+
// the bare objective (never the literal `/goal …` text the user typed)
|
|
255
|
+
// or shows a warning, so the normal submission path never records it in
|
|
256
|
+
// input history. Preserve the typed command whenever args were supplied
|
|
257
|
+
// — including the first-time `/goal set <objective>` case where goal
|
|
258
|
+
// mode was not yet active. A previous `wasGoalModeEnabled` guard dropped
|
|
259
|
+
// that first-time case from history (up/down-arrow recall).
|
|
256
260
|
await runtime.ctx.handleGoalModeCommand(command.args || undefined);
|
|
257
|
-
if (
|
|
261
|
+
if (command.args) {
|
|
258
262
|
runtime.ctx.editor.addToHistory(command.text);
|
|
259
263
|
}
|
|
260
264
|
runtime.ctx.editor.setText("");
|
package/src/system-prompt.ts
CHANGED
|
@@ -253,15 +253,17 @@ export async function loadProjectContextFiles(
|
|
|
253
253
|
|
|
254
254
|
const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
|
|
255
255
|
|
|
256
|
-
// Convert ContextFile items and preserve depth info
|
|
257
|
-
const files = result.items
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
256
|
+
// Convert project-level ContextFile items and preserve depth info
|
|
257
|
+
const files = result.items
|
|
258
|
+
.filter(item => (item as ContextFile).level === "project")
|
|
259
|
+
.map(item => {
|
|
260
|
+
const contextFile = item as ContextFile;
|
|
261
|
+
return {
|
|
262
|
+
path: contextFile.path,
|
|
263
|
+
content: contextFile.content,
|
|
264
|
+
depth: contextFile.depth,
|
|
265
|
+
};
|
|
266
|
+
});
|
|
265
267
|
|
|
266
268
|
// Sort by depth (descending): higher depth (farther from cwd) comes first,
|
|
267
269
|
// so files closer to cwd appear later and are more prominent
|