@agentprojectcontext/apx 1.14.1 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,12 @@ import {
13
13
 
14
14
  const MAIN_PALETTE_OPTIONS = ["Switch model", "Connect provider", "Open editor", "Exit"];
15
15
 
16
+ // Message Actions overlay options for a queued message
17
+ const MSG_ACTION_SEND = "Send now (interrupt current)";
18
+ const MSG_ACTION_COPY = "Copy message text";
19
+ const MSG_ACTION_QUESTION = "Ask about this...";
20
+ const MSG_ACTION_REMOVE = "Remove from queue";
21
+
16
22
  export async function cmdSys(args) {
17
23
  const pid = await resolveProjectId(args?.flags?.project);
18
24
  const cfg = readConfig();
@@ -34,16 +40,25 @@ export async function cmdSys(args) {
34
40
  usage: { input: 0, output: 0, percent: 0 },
35
41
  chatScrollOffset: 0,
36
42
  transcript: [],
43
+ // Message Actions overlay state
44
+ inMsgActions: false,
45
+ msgActionsTarget: null, // { text } of the targeted message
46
+ msgActionsSelection: 0,
47
+ msgActionsOptions: [MSG_ACTION_SEND, MSG_ACTION_COPY, MSG_ACTION_QUESTION, MSG_ACTION_REMOVE],
37
48
  };
38
49
 
39
50
  const previousMessages = [];
40
51
  const pendingPrompts = [];
41
52
  let restored = false;
42
53
  let isRequesting = false;
54
+ // AbortController for the current in-flight LLM request
55
+ let currentAbortCtrl = null;
43
56
 
44
57
  function restoreTerminal() {
45
58
  if (restored) return;
46
59
  restored = true;
60
+ // Disable mouse tracking before exit
61
+ process.stdout.write("\x1b[?1000l\x1b[?1015l\x1b[?1006l");
47
62
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
48
63
  process.stdout.write(C.reset + C.showCursor + C.resetBg + C.altOff);
49
64
  }
@@ -67,6 +82,8 @@ export async function cmdSys(args) {
67
82
  readline.emitKeypressEvents(process.stdin);
68
83
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
69
84
  process.stdout.write(C.altOn + C.setBgBlack + C.showCursor + C.bg);
85
+ // Enable xterm mouse button tracking (X10 + SGR extended for wide terminals)
86
+ process.stdout.write("\x1b[?1000h\x1b[?1015h\x1b[?1006h");
70
87
  process.once("exit", restoreTerminal);
71
88
  process.once("SIGINT", close);
72
89
  process.once("SIGTERM", close);
@@ -75,25 +92,72 @@ export async function cmdSys(args) {
75
92
 
76
93
  renderScreen();
77
94
 
95
+ // Handle raw mouse tracking bytes before readline keypress
96
+ process.stdin.on("data", (chunk) => {
97
+ const raw = typeof chunk === "string" ? chunk : chunk.toString("binary");
98
+ // SGR mouse: ESC [ < Pb ; Px ; Py M/m
99
+ const sgrMatch = raw.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
100
+ if (sgrMatch) {
101
+ const btn = parseInt(sgrMatch[1], 10);
102
+ const col = parseInt(sgrMatch[2], 10) - 1;
103
+ const row = parseInt(sgrMatch[3], 10) - 1;
104
+ const press = sgrMatch[4] === "M";
105
+ if (press && btn === 0) {
106
+ handleMouseClick(col, row, state, pendingPrompts, renderScreen, () => {
107
+ // interrupt callback: abort current request then flush queue
108
+ if (currentAbortCtrl) currentAbortCtrl.abort();
109
+ });
110
+ }
111
+ return;
112
+ }
113
+ });
114
+
78
115
  process.stdin.on("keypress", async (str, key) => {
79
116
  if (key.ctrl && key.name === "c") {
117
+ // If a request is running, interrupt it first; second Ctrl-C exits
118
+ if (isRequesting && currentAbortCtrl) {
119
+ currentAbortCtrl.abort();
120
+ return;
121
+ }
80
122
  close();
81
123
  }
82
124
 
125
+ // Ctrl+I = interrupt current request and immediately send first queued prompt
126
+ if (key.ctrl && key.name === "i" && isRequesting) {
127
+ if (currentAbortCtrl) currentAbortCtrl.abort();
128
+ return;
129
+ }
130
+
83
131
  if (key.ctrl && key.name === "p") {
84
132
  state.inCommandPalette = !state.inCommandPalette;
133
+ state.inMsgActions = false;
85
134
  resetPalette();
86
135
  renderScreen();
87
136
  return;
88
137
  }
89
138
 
90
- if (key.name === "escape" && state.inCommandPalette) {
91
- if (state.paletteState !== "main") {
92
- resetPalette();
93
- } else {
94
- state.inCommandPalette = false;
139
+ if (key.name === "escape") {
140
+ if (state.inMsgActions) {
141
+ state.inMsgActions = false;
142
+ state.msgActionsTarget = null;
143
+ renderScreen();
144
+ return;
95
145
  }
96
- renderScreen();
146
+ if (state.inCommandPalette) {
147
+ if (state.paletteState !== "main") {
148
+ resetPalette();
149
+ } else {
150
+ state.inCommandPalette = false;
151
+ }
152
+ renderScreen();
153
+ return;
154
+ }
155
+ }
156
+
157
+ if (state.inMsgActions) {
158
+ await handleMsgActionsKey(key, state, pendingPrompts, renderScreen, () => {
159
+ if (currentAbortCtrl) currentAbortCtrl.abort();
160
+ });
97
161
  return;
98
162
  }
99
163
 
@@ -105,14 +169,23 @@ export async function cmdSys(args) {
105
169
  if (handleScrollKey(key, state, renderScreen)) return;
106
170
 
107
171
  if (isReturnKey(key)) {
172
+ if (isExitCommand(state.inputText)) {
173
+ close();
174
+ return;
175
+ }
176
+
108
177
  if (isRequesting) {
109
178
  queuePrompt(state, pendingPrompts, renderScreen);
110
179
  return;
111
180
  }
112
181
 
113
182
  isRequesting = true;
114
- await submitPromptQueue(pid, state, previousMessages, pendingPrompts, renderScreen, close);
183
+ await submitPromptQueue(
184
+ pid, state, previousMessages, pendingPrompts, renderScreen, close,
185
+ (ctrl) => { currentAbortCtrl = ctrl; }
186
+ );
115
187
  isRequesting = false;
188
+ currentAbortCtrl = null;
116
189
  return;
117
190
  }
118
191
 
@@ -120,10 +193,123 @@ export async function cmdSys(args) {
120
193
  });
121
194
  }
122
195
 
196
+ // ---------------------------------------------------------------------------
197
+ // Mouse click → Message Actions overlay
198
+ // ---------------------------------------------------------------------------
199
+
200
+ /**
201
+ * Determine if a click at (col, row) lands on a queued user message bubble,
202
+ * and if so open the Message Actions overlay for it.
203
+ */
204
+ function handleMouseClick(col, row, state, pendingPrompts, renderScreen, onInterrupt) {
205
+ if (!state.hasStarted) return;
206
+
207
+ // Find the message bubble that was clicked.
208
+ // We look for both queued and regular messages in the transcript.
209
+ // Transcript is rendered from bottom to top in terms of logic,
210
+ // but we'll use a simple heuristic for now.
211
+ const allUserMessages = state.transcript.filter(t => t.type === "user");
212
+ if (allUserMessages.length === 0) return;
213
+
214
+ const { width } = { width: process.stdout.columns || 80 };
215
+ if (col > Math.floor(width * 0.8)) return;
216
+
217
+ // For now, just pick the most recent one if clicked in the main area.
218
+ // In a real app we'd map row to transcript index precisely.
219
+ state.inMsgActions = true;
220
+ state.msgActionsTarget = allUserMessages[allUserMessages.length - 1];
221
+ state.msgActionsSelection = 0;
222
+
223
+ // Filter options: "Send now" only for queued items
224
+ const isQueued = state.msgActionsTarget.meta === "queued";
225
+ state.msgActionsOptions = isQueued
226
+ ? [MSG_ACTION_SEND, MSG_ACTION_COPY, MSG_ACTION_QUESTION, MSG_ACTION_REMOVE]
227
+ : [MSG_ACTION_COPY, MSG_ACTION_QUESTION];
228
+
229
+ renderScreen();
230
+ }
231
+
232
+ /** Keyboard nav inside the Message Actions overlay */
233
+ async function handleMsgActionsKey(key, state, pendingPrompts, renderScreen, onInterrupt) {
234
+ if (key.name === "up") {
235
+ state.msgActionsSelection = Math.max(0, state.msgActionsSelection - 1);
236
+ renderScreen();
237
+ return;
238
+ }
239
+ if (key.name === "down") {
240
+ state.msgActionsSelection = Math.min(
241
+ state.msgActionsOptions.length - 1,
242
+ state.msgActionsSelection + 1
243
+ );
244
+ renderScreen();
245
+ return;
246
+ }
247
+ if (key.name !== "return") {
248
+ renderScreen();
249
+ return;
250
+ }
251
+
252
+ const selected = state.msgActionsOptions[state.msgActionsSelection];
253
+ const target = state.msgActionsTarget;
254
+
255
+ // Close overlay first
256
+ state.inMsgActions = false;
257
+ state.msgActionsTarget = null;
258
+
259
+ if (selected === MSG_ACTION_REMOVE) {
260
+ // Remove from pendingPrompts and transcript
261
+ const idx = pendingPrompts.findIndex((p) => p.text === target?.text);
262
+ if (idx >= 0) pendingPrompts.splice(idx, 1);
263
+ const tidx = state.transcript.indexOf(target);
264
+ if (tidx >= 0) state.transcript.splice(tidx, 1);
265
+ renderScreen();
266
+ return;
267
+ }
268
+
269
+ if (selected === MSG_ACTION_COPY) {
270
+ // Best-effort clipboard via pbcopy (macOS) / xclip (Linux)
271
+ try {
272
+ const { execSync } = await import("node:child_process");
273
+ const cmd = process.platform === "darwin" ? "pbcopy" : "xclip -selection clipboard";
274
+ execSync(cmd, { input: target?.text || "", stdio: ["pipe", "ignore", "ignore"] });
275
+ } catch {}
276
+ state.transcript.push({ type: "status", text: "Copied to clipboard" });
277
+ renderScreen();
278
+ return;
279
+ }
280
+
281
+ if (selected === MSG_ACTION_QUESTION) {
282
+ const text = target?.text || "";
283
+ state.inputText = `Pregunta sobre esto: "${text.slice(0, 50)}${text.length > 50 ? "..." : ""}"\n\n`;
284
+ state.cursorIndex = state.inputText.length;
285
+ renderScreen();
286
+ return;
287
+ }
288
+
289
+ if (selected === MSG_ACTION_SEND) {
290
+ // Promote the queued item to front of queue, then interrupt current request
291
+ const idx = pendingPrompts.findIndex((p) => p.text === target?.text);
292
+ if (idx > 0) {
293
+ const [entry] = pendingPrompts.splice(idx, 1);
294
+ pendingPrompts.unshift(entry);
295
+ }
296
+ // Signal the interrupt — the running submitPromptQueue will pick it up
297
+ onInterrupt();
298
+ renderScreen();
299
+ return;
300
+ }
301
+
302
+ renderScreen();
303
+ }
304
+
123
305
  export function isReturnKey(key) {
124
306
  return key?.name === "return" || key?.name === "enter";
125
307
  }
126
308
 
309
+ export function isExitCommand(text) {
310
+ return /^(exit|quit)$/i.test(String(text || "").trim());
311
+ }
312
+
127
313
  export function handleScrollKey(key, state, renderScreen) {
128
314
  if (!state.hasStarted || !key) return false;
129
315
  const pageSize = key.name === "pageup" || key.name === "pagedown" ? 8 : 3;
@@ -329,11 +515,15 @@ function queuePrompt(state, pendingPrompts, renderScreen) {
329
515
  renderScreen();
330
516
  }
331
517
 
332
- async function submitPromptQueue(pid, state, previousMessages, pendingPrompts, renderScreen, close) {
518
+ async function submitPromptQueue(
519
+ pid, state, previousMessages, pendingPrompts, renderScreen, close,
520
+ setAbortCtrl = () => {}
521
+ ) {
333
522
  const firstText = state.inputText.trim();
334
523
  if (!firstText) return;
335
- if (firstText.toLowerCase() === "exit" || firstText.toLowerCase() === "quit") {
524
+ if (isExitCommand(firstText)) {
336
525
  close();
526
+ return;
337
527
  }
338
528
 
339
529
  state.hasStarted = true;
@@ -343,20 +533,29 @@ async function submitPromptQueue(pid, state, previousMessages, pendingPrompts, r
343
533
 
344
534
  const firstItem = { type: "user", text: firstText };
345
535
  state.transcript.push(firstItem);
346
- await runPrompt(pid, state, previousMessages, renderScreen, firstText, firstItem);
536
+ await runPrompt(
537
+ pid, state, previousMessages, renderScreen, firstText, firstItem, setAbortCtrl
538
+ );
347
539
 
348
540
  while (pendingPrompts.length > 0) {
349
541
  const queued = pendingPrompts.shift();
350
542
  delete queued.item.meta;
351
- await runPrompt(pid, state, previousMessages, renderScreen, queued.text, queued.item);
543
+ await runPrompt(
544
+ pid, state, previousMessages, renderScreen, queued.text, queued.item, setAbortCtrl
545
+ );
352
546
  }
353
547
  }
354
548
 
355
- async function runPrompt(pid, state, previousMessages, renderScreen, text, userItem) {
549
+ async function runPrompt(
550
+ pid, state, previousMessages, renderScreen, text, userItem,
551
+ setAbortCtrl = () => {}
552
+ ) {
356
553
  appendLiveItem(state, { type: "status", text: "Thinking...", active: true });
357
554
  renderScreen();
358
555
 
359
556
  const startTime = Date.now();
557
+ const abortCtrl = http.createAbortController();
558
+ setAbortCtrl(abortCtrl);
360
559
 
361
560
  try {
362
561
  const cwd = process.cwd();
@@ -372,28 +571,49 @@ async function runPrompt(pid, state, previousMessages, renderScreen, text, userI
372
571
  };
373
572
 
374
573
  let result;
574
+ let interrupted = false;
375
575
  try {
376
576
  result = await http.streamPost(
377
577
  `/projects/${pid}/super-agent/chat/stream`,
378
578
  body,
379
- (event) => handleProgressEvent(event, state, renderScreen)
579
+ (event) => handleProgressEvent(event, state, renderScreen),
580
+ { signal: abortCtrl.signal }
380
581
  );
381
582
  } catch (e) {
382
- if (e.status !== 404) throw e;
383
- result = await http.post(`/projects/${pid}/super-agent/chat`, body);
384
- removeStatus(state);
385
- for (const trace of result.trace || []) {
386
- appendLiveItem(state, { type: "tool", trace });
583
+ if (abortCtrl.signal.aborted) {
584
+ // Interrupted by user — show notice and continue to next queued prompt
585
+ interrupted = true;
586
+ removeStatus(state);
587
+ appendLiveItem(state, {
588
+ type: "status",
589
+ text: `\u26a1 Interrupted — ${text.slice(0, 60)}${text.length > 60 ? "\u2026" : ""}`,
590
+ });
591
+ } else if (e.status !== 404) {
592
+ throw e;
593
+ } else {
594
+ result = await http.post(
595
+ `/projects/${pid}/super-agent/chat`, body,
596
+ { signal: abortCtrl.signal }
597
+ );
598
+ removeStatus(state);
599
+ for (const trace of result.trace || []) {
600
+ appendLiveItem(state, { type: "tool", trace });
601
+ }
387
602
  }
388
603
  }
389
604
 
390
- completeSuperAgentResult(result, text, startTime, state, previousMessages);
605
+ if (!interrupted && result) {
606
+ completeSuperAgentResult(result, text, startTime, state, previousMessages);
607
+ }
391
608
  } catch (e) {
392
- removeStatus(state);
393
- appendLiveItem(state, { type: "error", text: e.message });
609
+ if (!abortCtrl.signal.aborted) {
610
+ removeStatus(state);
611
+ appendLiveItem(state, { type: "error", text: e.message });
612
+ }
394
613
  }
395
614
 
396
615
  if (userItem) delete userItem.meta;
616
+ setAbortCtrl(null);
397
617
  renderScreen();
398
618
  }
399
619
 
@@ -424,6 +644,14 @@ function handleProgressEvent(event, state, renderScreen) {
424
644
  return;
425
645
  }
426
646
 
647
+ if (event.type === "model_retry") {
648
+ const status = [...state.transcript].reverse().find((item) => item?.type === "status" && item?.active);
649
+ if (status) status.text = "Retrying with tool fallback...";
650
+ else appendLiveItem(state, { type: "status", text: "Retrying with tool fallback...", active: true });
651
+ renderScreen();
652
+ return;
653
+ }
654
+
427
655
  if (event.type === "assistant_text" && event.text) {
428
656
  removeStatus(state);
429
657
  appendLiveItem(state, {
@@ -4,8 +4,14 @@ export async function cmdTelegramSend(args) {
4
4
  const text = args._[0];
5
5
  if (!text) throw new Error("apx telegram send: missing <text>");
6
6
  const chat_id = args.flags.chat === true ? undefined : args.flags.chat;
7
- const result = await http.post("/telegram/send", { chat_id, text });
8
- console.log(`✅ sent (message_id=${result.message_id})`);
7
+ // --interrupt / --force: send immediately bypassing any pending agent queue
8
+ const interrupt = !!(args.flags.interrupt || args.flags.force);
9
+ const result = await http.post("/telegram/send", { chat_id, text, interrupt });
10
+ if (interrupt) {
11
+ console.log(`⚡ sent (interrupt, message_id=${result.message_id})`);
12
+ } else {
13
+ console.log(`✅ sent (message_id=${result.message_id})`);
14
+ }
9
15
  }
10
16
 
11
17
  export async function cmdTelegramStatus() {
package/src/cli/http.js CHANGED
@@ -75,6 +75,7 @@ async function request(method, path, body, opts = {}) {
75
75
  method,
76
76
  headers: body ? { "content-type": "application/json" } : {},
77
77
  body: body ? JSON.stringify(body) : undefined,
78
+ signal: opts.signal,
78
79
  });
79
80
  const text = await res.text();
80
81
  let json;
@@ -101,6 +102,7 @@ async function streamRequest(method, path, body, onEvent, opts = {}) {
101
102
  method,
102
103
  headers: body ? { "content-type": "application/json" } : {},
103
104
  body: body ? JSON.stringify(body) : undefined,
105
+ signal: opts.signal,
104
106
  });
105
107
 
106
108
  if (!res.ok) {
@@ -121,10 +123,21 @@ async function streamRequest(method, path, body, onEvent, opts = {}) {
121
123
  let buffer = "";
122
124
  let finalResult = null;
123
125
 
126
+ // Register abort handler to cancel the reader
127
+ if (opts.signal) {
128
+ opts.signal.addEventListener("abort", () => reader.cancel().catch(() => {}), { once: true });
129
+ }
130
+
124
131
  while (true) {
125
- const { value, done } = await reader.read();
126
- if (done) break;
127
- buffer += decoder.decode(value, { stream: true });
132
+ let chunk;
133
+ try {
134
+ chunk = await reader.read();
135
+ } catch (e) {
136
+ // AbortError or cancel — treat as clean end
137
+ break;
138
+ }
139
+ if (chunk.done) break;
140
+ buffer += decoder.decode(chunk.value, { stream: true });
128
141
  const lines = buffer.split(/\r?\n/);
129
142
  buffer = lines.pop() || "";
130
143
  for (const line of lines) {
@@ -138,10 +151,12 @@ async function streamRequest(method, path, body, onEvent, opts = {}) {
138
151
 
139
152
  buffer += decoder.decode();
140
153
  if (buffer.trim()) {
141
- const event = JSON.parse(buffer);
142
- if (event.type === "final") finalResult = event.result;
143
- if (event.type === "error") throw new Error(event.error || "stream error");
144
- await onEvent?.(event);
154
+ try {
155
+ const event = JSON.parse(buffer);
156
+ if (event.type === "final") finalResult = event.result;
157
+ if (event.type === "error") throw new Error(event.error || "stream error");
158
+ await onEvent?.(event);
159
+ } catch {}
145
160
  }
146
161
 
147
162
  return finalResult;
@@ -156,4 +171,6 @@ export const http = {
156
171
  delete: (p, opts) => request("DELETE", p, undefined, opts),
157
172
  baseUrl,
158
173
  ping,
174
+ /** Create a fresh AbortController for cancelling in-flight requests. */
175
+ createAbortController: () => new AbortController(),
159
176
  };
package/src/cli/index.js CHANGED
@@ -607,9 +607,16 @@ const HELP_TOPICS = new Map(Object.entries({
607
607
  "telegram send": topic({
608
608
  title: "apx telegram send",
609
609
  summary: "Send a Telegram message through the configured bridge.",
610
- usage: ["apx telegram send \"<text>\" [--chat <id>]"],
611
- options: [["--chat <id>", "Override configured chat id."]],
612
- examples: ["apx telegram send \"Deploy finished\" --chat 123456"],
610
+ usage: ["apx telegram send \"<text>\" [--chat <id>] [--interrupt]"],
611
+ options: [
612
+ ["--chat <id>", "Override configured chat id."],
613
+ ["--interrupt", "Send immediately, bypassing any pending agent queue (alias: --force)."],
614
+ ["--force", "Alias for --interrupt."],
615
+ ],
616
+ examples: [
617
+ "apx telegram send \"Deploy finished\" --chat 123456",
618
+ "apx telegram send \"Urgent!\" --interrupt",
619
+ ],
613
620
  }),
614
621
  "telegram status": topic({
615
622
  title: "apx telegram status",
@@ -1,10 +1,55 @@
1
1
  #!/usr/bin/env node
2
- // Runs automatically after `npm install -g apx`.
3
- // Installs APX + APC context skills to all global skill directories.
4
- import { installGlobalSkills } from "../core/scaffold.js";
2
+ // Runs automatically after `npm install -g apx` and `npm update -g apx`.
3
+ //
4
+ // Two-step process:
5
+ // 1. Refresh the bundled `apc-context` skill from the canonical APC repo.
6
+ // APC is a living standard — we always want the latest copy at install
7
+ // time. If the network call fails, the bundled snapshot that ships
8
+ // with the npm tarball is used.
9
+ // 2. Propagate APX + APC skills (+ runtime docs) into every global skill
10
+ // directory (~/.claude/skills, ~/.cursor/skills, ~/.codex/skills,
11
+ // ~/.agents/skills).
12
+ import fs from "node:fs";
13
+ import path from "node:path";
5
14
  import os from "node:os";
15
+ import { fileURLToPath } from "node:url";
16
+ import { installGlobalSkills } from "../core/scaffold.js";
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
20
+ const APC_SKILL_LOCAL = path.join(PACKAGE_ROOT, "skills", "apc-context", "SKILL.md");
21
+ const APC_SKILL_REMOTE =
22
+ "https://raw.githubusercontent.com/agentprojectcontext/agentprojectcontext/main/skills/apc-context/SKILL.md";
23
+
24
+ async function refreshApcSkill() {
25
+ if (process.env.APX_SKIP_SKILL_REFRESH) return { status: "skipped" };
26
+
27
+ try {
28
+ const fetchImpl = globalThis.fetch || (await import("node-fetch")).default;
29
+ const ac = new AbortController();
30
+ const timer = setTimeout(() => ac.abort(), 5000);
31
+
32
+ const res = await fetchImpl(APC_SKILL_REMOTE, { signal: ac.signal });
33
+ clearTimeout(timer);
34
+
35
+ if (!res.ok) return { status: "fallback", reason: `HTTP ${res.status}` };
36
+ const text = await res.text();
37
+
38
+ if (!text.startsWith("---") || !/name:\s*apc-context/.test(text)) {
39
+ return { status: "fallback", reason: "remote payload not a SKILL.md" };
40
+ }
41
+
42
+ fs.mkdirSync(path.dirname(APC_SKILL_LOCAL), { recursive: true });
43
+ fs.writeFileSync(APC_SKILL_LOCAL, text, "utf8");
44
+ return { status: "refreshed" };
45
+ } catch (err) {
46
+ return { status: "fallback", reason: err?.message || String(err) };
47
+ }
48
+ }
6
49
 
7
50
  try {
51
+ const refresh = await refreshApcSkill();
52
+
8
53
  const results = installGlobalSkills();
9
54
  if (results.length === 0) process.exit(0);
10
55
 
@@ -19,7 +64,12 @@ try {
19
64
  for (const [skill, dirs] of Object.entries(bySkill)) {
20
65
  console.log(` ${skill.padEnd(14)} → ${dirs.join(", ")}`);
21
66
  }
67
+ if (refresh.status === "refreshed") {
68
+ console.log(" apc-context refreshed from agentprojectcontext/agentprojectcontext@main");
69
+ } else if (refresh.status === "fallback") {
70
+ console.log(` apc-context: using bundled snapshot (${refresh.reason})`);
71
+ }
22
72
  console.log("");
23
73
  } catch {
24
- // Non-fatal — don't break the install
74
+ // Non-fatal — don't break the install.
25
75
  }
@@ -173,6 +173,12 @@ function renderPromptBlock(state, chatWidth) {
173
173
  const hotkeys =
174
174
  C.bold + C.text + "tab" + C.normal + C.muted + " agents " +
175
175
  C.bold + C.text + "ctrl+p" + C.normal + C.muted + " commands " +
176
+ (state.hasStarted && state.transcript?.some((t) => t.type === "user" && t.meta === "queued")
177
+ ? C.bold + C.warning + "ctrl+i" + C.normal + C.muted + " interrupt "
178
+ : "") +
179
+ (state.hasStarted && state.transcript?.length > 0
180
+ ? C.bold + C.text + "click" + C.normal + C.muted + " actions "
181
+ : "") +
176
182
  C.bold + C.text + "enter" + C.normal + C.muted + " send";
177
183
  const hotkeyLeft = Math.max(left, left + boxWidth - visible(hotkeys));
178
184
  writeAt(top + 5, hotkeyLeft, hotkeys, visible(hotkeys), C.bg);
@@ -276,7 +282,11 @@ function transcriptLines(transcript, width) {
276
282
  addLine(lines, margin + C.primary + "┃" + C.panel + " " + C.text + padAnsi(chunk, inner), C.bg);
277
283
  }
278
284
  if (item.meta) {
279
- addLine(lines, margin + C.primary + "┃" + C.panel + " " + C.muted + padAnsi(item.meta, inner), C.bg);
285
+ // Show QUEUED badge with distinct highlight color
286
+ const badgeText = item.meta === "queued"
287
+ ? C.warning + C.bold + "QUEUED" + C.normal + C.muted + " click to send/remove"
288
+ : C.muted + item.meta;
289
+ addLine(lines, margin + C.primary + "┃" + C.panel + " " + badgeText + padAnsi("", Math.max(0, inner - (item.meta === "queued" ? 28 : visible(item.meta)))), C.bg);
280
290
  }
281
291
  addLine(lines, margin + C.primary + "┃" + C.panel + " " + " ".repeat(inner), C.bg);
282
292
  continue;
@@ -301,7 +311,23 @@ function transcriptLines(transcript, width) {
301
311
  }
302
312
 
303
313
  if (item.type === "tool") {
304
- addToolBlock(lines, item, width);
314
+ const trace = item.trace || {};
315
+ const isQuestion = trace.tool === "ask_questions";
316
+ const label = isQuestion ? C.warning + C.bold + "QUESTION" : C.muted + "TOOL";
317
+ const name = isQuestion ? "" : C.text + trace.tool;
318
+
319
+ if (isQuestion && trace.args?.questions) {
320
+ addLine(lines, "", C.bg);
321
+ addLine(lines, margin + label + C.muted + " (…)" + C.bg, C.bg);
322
+ for (const q of trace.args.questions) {
323
+ const qWrapped = wrapText(`• ${q}`, inner - 2);
324
+ for (const line of qWrapped) {
325
+ addLine(lines, margin + C.primary + "┃ " + C.text + padAnsi(line, inner - 2), C.bg);
326
+ }
327
+ }
328
+ } else {
329
+ addLine(lines, margin + label + " " + name + C.bg, C.bg);
330
+ }
305
331
  continue;
306
332
  }
307
333
 
@@ -408,6 +434,36 @@ function renderPaletteOverlay(state) {
408
434
  );
409
435
  }
410
436
 
437
+ function renderMsgActionsOverlay(state) {
438
+ const { width, height } = terminalSize();
439
+ const title = "Message Actions";
440
+ const opts = state.msgActionsOptions;
441
+ const preview = fit(state.msgActionsTarget?.text || "", 42);
442
+ const boxWidth = Math.min(62, Math.max(title.length + 8, preview.length + 6, ...opts.map((x) => visible(x) + 8)));
443
+ const boxHeight = opts.length + 5;
444
+ const left = centerLeft(width, boxWidth);
445
+ const top = Math.max(1, Math.floor((height - boxHeight) / 2));
446
+
447
+ writeAt(top, left, C.text + C.bold + " " + title + C.normal, boxWidth, C.panel);
448
+ writeAt(top + 1, left, C.muted + " " + C.italic + preview + C.noItalic, boxWidth, C.panel);
449
+ writeAt(top + 2, left, C.dim + "▀".repeat(boxWidth), boxWidth, C.panel);
450
+ for (let i = 0; i < opts.length; i++) {
451
+ const active = i === state.msgActionsSelection;
452
+ const marker = active ? "›" : " ";
453
+ const bg = active ? C.panel2 : C.panel;
454
+ const fg = active ? C.primary + C.bold : C.text;
455
+ writeAt(top + 3 + i, left, fg + ` ${marker} ${opts[i]}` + C.normal, boxWidth, bg);
456
+ }
457
+ writeAt(top + 3 + opts.length, left, C.dim + "▄".repeat(boxWidth), boxWidth, C.panel);
458
+ writeAt(
459
+ top + 4 + opts.length,
460
+ left,
461
+ C.muted + "↑↓ select " + C.text + C.bold + "enter" + C.normal + C.muted + " choose " + C.text + C.bold + "esc" + C.normal + C.muted + " close",
462
+ boxWidth,
463
+ C.bg
464
+ );
465
+ }
466
+
411
467
  export function renderTerminalChat(state) {
412
468
  clearFull();
413
469
  const { width, height } = terminalSize();
@@ -424,5 +480,6 @@ export function renderTerminalChat(state) {
424
480
  const cursor = renderPromptBlock(state, chatWidth);
425
481
 
426
482
  if (state.inCommandPalette) renderPaletteOverlay(state);
427
- if (!state.inCommandPalette) moveTo(cursor.row, cursor.col);
483
+ if (state.inMsgActions) renderMsgActionsOverlay(state);
484
+ if (!state.inCommandPalette && !state.inMsgActions) moveTo(cursor.row, cursor.col);
428
485
  }