@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.
@@ -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, () => void this.handleFollowUp());
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
- // No image in clipboard - show hint
828
- this.ctx.showStatus("No image in clipboard (use terminal paste for text)");
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 { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
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].join(" ").toLowerCase();
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 fs.promises.rename(oldSessionFile, newSessionFile);
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 fs.promises.rename(oldArtifactDir, newArtifactDir);
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
- const hadArgs = !!command.args;
254
- // Capture state BEFORE the call (see /plan above for rationale).
255
- const wasGoalModeEnabled = runtime.ctx.goalModeEnabled;
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 (hadArgs && wasGoalModeEnabled) {
261
+ if (command.args) {
258
262
  runtime.ctx.editor.addToHistory(command.text);
259
263
  }
260
264
  runtime.ctx.editor.setText("");
@@ -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.map(item => {
258
- const contextFile = item as ContextFile;
259
- return {
260
- path: contextFile.path,
261
- content: contextFile.content,
262
- depth: contextFile.depth,
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