mixdog 0.9.93 → 0.9.94

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.
Files changed (52) hide show
  1. package/package.json +1 -1
  2. package/src/rules/agent/30-explorer.md +22 -16
  3. package/src/rules/shared/01-tool.md +23 -18
  4. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +22 -6
  5. package/src/runtime/agent/orchestrator/context/collect.mjs +6 -2
  6. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
  7. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
  8. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +100 -86
  9. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
  10. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
  11. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +3 -3
  12. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +4 -1
  13. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -4
  14. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +12 -3
  15. package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +22 -0
  16. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +491 -0
  17. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +91 -13
  18. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +90 -27
  19. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +6 -1
  20. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
  21. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
  22. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +7 -2
  23. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +32 -4
  24. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +16 -1
  25. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +543 -19
  26. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +5 -2
  27. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
  28. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +48 -0
  29. package/src/runtime/agent/orchestrator/tools/builtin.mjs +71 -1
  30. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +4 -2
  31. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +51 -2
  32. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +6 -17
  33. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -4
  34. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +3 -3
  35. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +3 -3
  36. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
  37. package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
  38. package/src/runtime/shared/background-tasks.mjs +10 -3
  39. package/src/runtime/shared/child-spawn-gate.mjs +50 -26
  40. package/src/runtime/shared/task-notification-envelope.mjs +11 -2
  41. package/src/runtime/shared/tool-card-model.mjs +6 -2
  42. package/src/runtime/shared/tool-surface.mjs +7 -2
  43. package/src/session-runtime/provider-usage.mjs +26 -2
  44. package/src/standalone/explore-tool.mjs +1 -1
  45. package/src/tui/app/use-transcript-window.mjs +7 -1
  46. package/src/tui/components/Spinner.jsx +18 -9
  47. package/src/tui/dist/index.mjs +111 -25
  48. package/src/tui/engine/live-share.mjs +23 -3
  49. package/src/tui/engine/session-api.mjs +7 -0
  50. package/src/tui/engine/turn.mjs +77 -4
  51. package/src/tui/engine.mjs +14 -6
  52. package/src/tui/index.jsx +7 -0
@@ -24,6 +24,15 @@ export {
24
24
  backgroundTaskHeaderStatus,
25
25
  };
26
26
 
27
+ // The full command is already visible in the start response / task record;
28
+ // the envelope only needs an identifying prefix. Flatten whitespace and cap
29
+ // so multi-line commands do not re-echo hundreds of chars per notification.
30
+ function compactCommand(command) {
31
+ if (!command) return null;
32
+ const flat = String(command).replace(/\s+/g, ' ').trim();
33
+ return flat.length > 160 ? `${flat.slice(0, 160)}…` : flat;
34
+ }
35
+
27
36
  // Render the bracketed shell *completion* envelope body.
28
37
  // Byte-compatible with the historical inline assembly in shell-jobs.fire().
29
38
  export function renderShellCompletionEnvelope({
@@ -42,7 +51,7 @@ export function renderShellCompletionEnvelope({
42
51
  `[status: ${status}]`,
43
52
  `[exit: ${exitCode === null ? 'n/a' : exitCode}]`,
44
53
  elapsedMs !== null ? `[elapsed: ${elapsedMs} ms]` : null,
45
- command ? `[command: ${command}]` : null,
54
+ command ? `[command: ${compactCommand(command)}]` : null,
46
55
  ].filter((l) => l !== null);
47
56
  const bodySections = [
48
57
  summary ? `Summary: ${summary}` : null,
@@ -86,7 +95,7 @@ export function renderShellPromptStallEnvelope({
86
95
  '[status: running]',
87
96
  `[stalled: no output growth for ${stalledMs} ms]`,
88
97
  (elapsedMs !== null && elapsedMs >= 0) ? `[elapsed: ${elapsedMs} ms]` : null,
89
- command ? `[command: ${command}]` : null,
98
+ command ? `[command: ${compactCommand(command)}]` : null,
90
99
  '',
91
100
  'This background shell task appears to be waiting for interactive input. Background tasks cannot answer prompts automatically; cancel it or rerun with non-interactive flags/input.',
92
101
  tailText ? `\n${tailText}` : null,
@@ -160,9 +160,13 @@ export function shellDisplayStatus({ pending = false, failedCount = 0, exitFaile
160
160
  return 'completed';
161
161
  }
162
162
 
163
- export function shellHeader(status, count = 1) {
163
+ export function shellHeader(status, count = 1, verifying = false) {
164
164
  const n = Math.max(1, Number(count) || 1);
165
165
  const object = `${n} ${plural(n, 'command')}`;
166
+ // A shell that follows an edit in the same batch IS the verification —
167
+ // label it as such while healthy; failures keep the neutral Ran + detail.
168
+ if (verifying && status === 'running') return `Verifying ${object}`;
169
+ if (verifying && status === 'completed') return `Verified ${object}`;
166
170
  if (status === 'running') return `Running ${object}`;
167
171
  return `Ran ${object}`;
168
172
  }
@@ -708,7 +712,7 @@ export function deriveToolCardModel(input = {}, options = {}) {
708
712
  if (isAgentResponse) labelText = agentResponseTitle(parsedArgs, groupCount);
709
713
  else if (isBackgroundResponse) labelText = backgroundTaskResultTitle(normalizedName, backgroundMeta || parsedArgs);
710
714
  else if (isBackgroundMetadataResult) labelText = backgroundTaskActionTitle(normalizedName, backgroundMeta);
711
- else if (isShellSurface) labelText = shellHeader(shellStatus, groupCount);
715
+ else if (isShellSurface) labelText = shellHeader(shellStatus, groupCount, parsedArgs?.verifyShell === true);
712
716
  else labelText = (isAgentTool(normalizedName) ? agentActionTitle(parsedArgs) : '')
713
717
  || formatToolActionHeader(name, args, { pending: headerPending, count: groupCount });
714
718
  labelText = safeInlineText(labelText);
@@ -467,10 +467,15 @@ export function toolWorkUnit(name, args = {}, category = '') {
467
467
  noun: 'file',
468
468
  });
469
469
  }
470
+ // post_shell runs a verification command inside the SAME tool call after
471
+ // the patch applies; without the suffix the spinner sits on "Editing 1
472
+ // file" for the whole (possibly long) verify run and reads like a stuck
473
+ // edit. Surface both phases in the active label.
474
+ const verifying = typeof a.post_shell === 'string' && a.post_shell.trim() !== '';
470
475
  return unitDescriptor('Patch', {
471
476
  count: patchFileCount(a) || 1,
472
- active: creating ? 'Creating' : deleting ? 'Deleting' : 'Editing',
473
- done: creating ? 'Created' : deleting ? 'Deleted' : 'Edited',
477
+ active: (creating ? 'Creating' : deleting ? 'Deleting' : 'Editing') + (verifying ? ' & verifying' : ''),
478
+ done: (creating ? 'Created' : deleting ? 'Deleted' : 'Edited') + (verifying ? ' & verified' : ''),
474
479
  noun: 'file',
475
480
  });
476
481
  }
@@ -6,6 +6,10 @@
6
6
  // live-binding is preserved.
7
7
  import { clean } from './session-text.mjs';
8
8
 
9
+ // A post-redeem dashboard rebuild is best effort: past this budget the caller
10
+ // gets its confirmed outcome and the surface revalidates on its own cadence.
11
+ const REDEEM_REFRESH_BUDGET_MS = 15_000;
12
+
9
13
  export function createProviderUsage({
10
14
  caches,
11
15
  getConfig,
@@ -134,9 +138,29 @@ export function createProviderUsage({
134
138
  const providerObj = reg().getProvider('openai-oauth');
135
139
  if (!providerObj) throw new Error('Codex is not signed in');
136
140
  const result = await consumeOpenAICodexResetCredit(providerObj, options);
141
+ // The redeem is DONE and its outcome is authoritative. Rebuilding the whole
142
+ // dashboard is a COURTESY refresh: it forces a sweep across every provider,
143
+ // so binding the answer to it once turned a spent credit into "reset could
144
+ // not be confirmed" whenever that sweep was slow or failed.
137
145
  caches.usageDashboardCache = {};
138
- const dashboard = await getUsageDashboard({ refresh: true, refreshSetup: false });
139
- return { ...result, dashboard };
146
+ const dashboard = await refreshedUsageDashboardWithin(REDEEM_REFRESH_BUDGET_MS);
147
+ return { ...result, ...(dashboard ? { dashboard } : {}) };
148
+ }
149
+
150
+ async function refreshedUsageDashboardWithin(budgetMs) {
151
+ let timer = null;
152
+ const budget = new Promise((resolve) => {
153
+ timer = setTimeout(() => resolve(null), budgetMs);
154
+ timer?.unref?.();
155
+ });
156
+ try {
157
+ return await Promise.race([
158
+ getUsageDashboard({ refresh: true, refreshSetup: false }).catch(() => null),
159
+ budget,
160
+ ]);
161
+ } finally {
162
+ if (timer) clearTimeout(timer);
163
+ }
140
164
  }
141
165
 
142
166
  return {
@@ -34,7 +34,7 @@ export const EXPLORE_TOOL = {
34
34
  // (50MB) plus a smart-read summariser downstream; the standalone path returns
35
35
  // the merged text in-turn, so we keep a sane bound to protect the Lead context.
36
36
  const EXPLORE_OUTPUT_CHAR_CAP = 24_000;
37
- const EXPLORE_TRUNCATION_MARKER = '\n\n[explore: output truncated; narrow cwd or split queries to see more]';
37
+ const EXPLORE_TRUNCATION_MARKER = '\n\n[explore: output capped; remainder omitted]';
38
38
  // Bound fan-out so a hostile/poisoned query array cannot spawn unbounded subs.
39
39
  export const MAX_FANOUT_QUERIES = 8;
40
40
  // Mechanical turn cap per explorer sub-session: 3 free turns (contract: tool
@@ -769,7 +769,13 @@ export function useTranscriptWindow({
769
769
  stopSmoothScroll();
770
770
  scrollTargetRef.current = desired;
771
771
  scrollPositionRef.current = Math.max(0, Math.min(maxRows, currentPosition + appliedDelta));
772
- setScrollOffset(Math.max(0, Math.round(desired)));
772
+ // The render path already applied `desired` synchronously through the
773
+ // absolute anchor lock. Keep the refs as the committed scroll authority;
774
+ // mirroring the same correction back into React state from a layout effect
775
+ // creates a render → layout update feedback path when transcript geometry
776
+ // and viewport geometry change together (React #185). The next manual
777
+ // scroll writes state from these refs, while anchored renders continue to
778
+ // resolve directly from the current prefix table.
773
779
  }, [transcriptWindow.totalRows, transcriptWindow.maxScrollRows, transcriptRowIndex, transcriptContentHeight, scrollOffset, stopSmoothScroll]);
774
780
  useLayoutEffect(() => {
775
781
  if (transcriptAnchorRef.current || transcriptAnchorDirtyRef.current || followingRef.current) return;
@@ -44,19 +44,25 @@ const GLIMMER_SPEED_MS = { requesting: 70, reconnecting: 70, compacting: 120, 'a
44
44
  const GLIMMER_TRAIL = 4;
45
45
  const THINKING_GLIMMER_SPEED_MS = 120;
46
46
  const THINKING_GLIMMER_TRAIL = 4;
47
- const VERB_ROTATE_MIN_MS = 10000;
48
- const VERB_ROTATE_SPREAD_MS = 10000;
49
- const VERB_CHANGE_PROBABILITY = 0.65;
47
+ // Fixed 30s cadence (user decision): the verb rotates deterministically every
48
+ // 30 seconds instead of the earlier randomized 10–20s / 65% coin flip that
49
+ // often never visibly fired.
50
+ const VERB_ROTATE_MIN_MS = 30000;
51
+ const VERB_ROTATE_SPREAD_MS = 0;
52
+ const VERB_CHANGE_PROBABILITY = 1;
50
53
 
51
54
  const MODE_VERBS = {
52
- requesting: ['Requesting', 'Preparing', 'Routing'],
55
+ requesting: ['Requesting'],
53
56
  compacting: ['Compacting conversation'],
54
57
  'auto-clear': ['Auto-clearing conversation'],
55
58
  resuming: ['Resuming conversation'],
56
- thinking: ['Thinking', 'Reasoning', 'Mapping'],
57
- 'tool-use': ['Using tools', 'Checking files', 'Running tools', 'Reading output'],
58
- 'tool-input': ['Using tools', 'Checking files', 'Running tools', 'Reading output'],
59
- responding: ['Responding', 'Composing', 'Writing', 'Wrapping up'],
59
+ // Conservative phrasing (user decision): each pool only rotates through
60
+ // synonyms of what the mode ACTUALLY means — never a concrete action the
61
+ // engine may not be doing (no "Checking files" during a web search).
62
+ thinking: ['Thinking', 'Considering', 'Organizing'],
63
+ 'tool-use': ['Working', 'Running tools', 'Reviewing output'],
64
+ 'tool-input': ['Working', 'Running tools', 'Reviewing output'],
65
+ responding: ['Writing', 'Wrapping up'],
60
66
  };
61
67
 
62
68
  function interpolateColor(a, b, t) {
@@ -232,7 +238,10 @@ export function Spinner({ verb = 'Working', startedAt, outputTokens = 0, tokens
232
238
  // --- Verb shimmer (traveling highlight) ---
233
239
  if (!displayVerbRef.current || displayVerbModeRef.current !== mode) {
234
240
  displayVerbRef.current = stableModeVerb(mode, verb);
235
- nextVerbCheckRef.current = nextVerbCheckAt(now);
241
+ // Mode flips (thinking→tool-use→responding…) must NOT re-arm the timer:
242
+ // frequent flips otherwise reset the countdown forever and the rotation
243
+ // never fires. The cadence holds from the first mount of the turn.
244
+ if (!nextVerbCheckRef.current) nextVerbCheckRef.current = nextVerbCheckAt(now);
236
245
  }
237
246
  displayVerbModeRef.current = mode;
238
247
  if (now >= nextVerbCheckRef.current) {
@@ -5273,10 +5273,11 @@ function toolWorkUnit(name, args = {}, category = "") {
5273
5273
  noun: "file"
5274
5274
  });
5275
5275
  }
5276
+ const verifying = typeof a.post_shell === "string" && a.post_shell.trim() !== "";
5276
5277
  return unitDescriptor("Patch", {
5277
5278
  count: patchFileCount(a) || 1,
5278
- active: creating ? "Creating" : deleting ? "Deleting" : "Editing",
5279
- done: creating ? "Created" : deleting ? "Deleted" : "Edited",
5279
+ active: (creating ? "Creating" : deleting ? "Deleting" : "Editing") + (verifying ? " & verifying" : ""),
5280
+ done: (creating ? "Created" : deleting ? "Deleted" : "Edited") + (verifying ? " & verified" : ""),
5280
5281
  noun: "file"
5281
5282
  });
5282
5283
  }
@@ -5927,18 +5928,21 @@ var GLIMMER_SPEED_MS = { requesting: 70, reconnecting: 70, compacting: 120, "aut
5927
5928
  var GLIMMER_TRAIL = 4;
5928
5929
  var THINKING_GLIMMER_SPEED_MS = 120;
5929
5930
  var THINKING_GLIMMER_TRAIL = 4;
5930
- var VERB_ROTATE_MIN_MS = 1e4;
5931
- var VERB_ROTATE_SPREAD_MS = 1e4;
5932
- var VERB_CHANGE_PROBABILITY = 0.65;
5931
+ var VERB_ROTATE_MIN_MS = 3e4;
5932
+ var VERB_ROTATE_SPREAD_MS = 0;
5933
+ var VERB_CHANGE_PROBABILITY = 1;
5933
5934
  var MODE_VERBS = {
5934
- requesting: ["Requesting", "Preparing", "Routing"],
5935
+ requesting: ["Requesting"],
5935
5936
  compacting: ["Compacting conversation"],
5936
5937
  "auto-clear": ["Auto-clearing conversation"],
5937
5938
  resuming: ["Resuming conversation"],
5938
- thinking: ["Thinking", "Reasoning", "Mapping"],
5939
- "tool-use": ["Using tools", "Checking files", "Running tools", "Reading output"],
5940
- "tool-input": ["Using tools", "Checking files", "Running tools", "Reading output"],
5941
- responding: ["Responding", "Composing", "Writing", "Wrapping up"]
5939
+ // Conservative phrasing (user decision): each pool only rotates through
5940
+ // synonyms of what the mode ACTUALLY means — never a concrete action the
5941
+ // engine may not be doing (no "Checking files" during a web search).
5942
+ thinking: ["Thinking", "Considering", "Organizing"],
5943
+ "tool-use": ["Working", "Running tools", "Reviewing output"],
5944
+ "tool-input": ["Working", "Running tools", "Reviewing output"],
5945
+ responding: ["Writing", "Wrapping up"]
5942
5946
  };
5943
5947
  function interpolateColor(a, b, t) {
5944
5948
  return {
@@ -6062,7 +6066,7 @@ function Spinner({ verb = "Working", startedAt, outputTokens = 0, tokens = 0, th
6062
6066
  )) : theme.spinnerGlyph;
6063
6067
  if (!displayVerbRef.current || displayVerbModeRef.current !== mode) {
6064
6068
  displayVerbRef.current = stableModeVerb(mode, verb);
6065
- nextVerbCheckRef.current = nextVerbCheckAt(now);
6069
+ if (!nextVerbCheckRef.current) nextVerbCheckRef.current = nextVerbCheckAt(now);
6066
6070
  }
6067
6071
  displayVerbModeRef.current = mode;
6068
6072
  if (now >= nextVerbCheckRef.current) {
@@ -16356,9 +16360,11 @@ function shellDisplayStatus({ pending = false, failedCount = 0, exitFailedCount
16356
16360
  if (isError || failedCount > 0) return "failed";
16357
16361
  return "completed";
16358
16362
  }
16359
- function shellHeader(status, count = 1) {
16363
+ function shellHeader(status, count = 1, verifying = false) {
16360
16364
  const n = Math.max(1, Number(count) || 1);
16361
16365
  const object = `${n} ${plural(n, "command")}`;
16366
+ if (verifying && status === "running") return `Verifying ${object}`;
16367
+ if (verifying && status === "completed") return `Verified ${object}`;
16362
16368
  if (status === "running") return `Running ${object}`;
16363
16369
  return `Ran ${object}`;
16364
16370
  }
@@ -16825,7 +16831,7 @@ function deriveToolCardModel(input = {}, options = {}) {
16825
16831
  if (isAgentResponse) labelText = agentResponseTitle(parsedArgs, groupCount);
16826
16832
  else if (isBackgroundResponse) labelText = backgroundTaskResultTitle(normalizedName, backgroundMeta || parsedArgs);
16827
16833
  else if (isBackgroundMetadataResult) labelText = backgroundTaskActionTitle(normalizedName, backgroundMeta);
16828
- else if (isShellSurface) labelText = shellHeader(shellStatus, groupCount);
16834
+ else if (isShellSurface) labelText = shellHeader(shellStatus, groupCount, parsedArgs?.verifyShell === true);
16829
16835
  else labelText = (isAgentTool(normalizedName) ? agentActionTitle(parsedArgs) : "") || formatToolActionHeader(name, args, { pending: headerPending, count: groupCount });
16830
16836
  labelText = safeInlineText(labelText);
16831
16837
  const toolSearchSummary = !pending && normalizedName === "load_tool" && hasResult ? toolSearchLoadedSummary(displayedResultText) : "";
@@ -22926,7 +22932,6 @@ function useTranscriptWindow({
22926
22932
  stopSmoothScroll();
22927
22933
  scrollTargetRef.current = desired;
22928
22934
  scrollPositionRef.current = Math.max(0, Math.min(maxRows, currentPosition + appliedDelta));
22929
- setScrollOffset(Math.max(0, Math.round(desired)));
22930
22935
  }, [transcriptWindow.totalRows, transcriptWindow.maxScrollRows, transcriptRowIndex, transcriptContentHeight, scrollOffset, stopSmoothScroll]);
22931
22936
  useLayoutEffect4(() => {
22932
22937
  if (transcriptAnchorRef.current || transcriptAnchorDirtyRef.current || followingRef.current) return;
@@ -27638,6 +27643,7 @@ function createRunTurn(bag) {
27638
27643
  const toolCards = [];
27639
27644
  const toolGroups = /* @__PURE__ */ new Map();
27640
27645
  const resultsDone = /* @__PURE__ */ new Set();
27646
+ const postShellTwins = /* @__PURE__ */ new Map();
27641
27647
  let liveTailTimer = null;
27642
27648
  let lastLiveTailPatched = "";
27643
27649
  const clearLiveTailTimer = () => {
@@ -27898,7 +27904,10 @@ function createRunTurn(bag) {
27898
27904
  const syncAggregateHeader = (aggregate) => {
27899
27905
  if (!aggregate?.itemId) return;
27900
27906
  const patch = {
27901
- args: { categoryOrder: aggregate.categoryOrder.slice() },
27907
+ args: {
27908
+ categoryOrder: aggregate.categoryOrder.slice(),
27909
+ ...aggregate.verifyShell ? { verifyShell: true } : {}
27910
+ },
27902
27911
  count: aggregate.calls.size,
27903
27912
  completedCount: [...aggregate.calls.values()].filter((r) => r.resolved || r.completedEarly).length,
27904
27913
  categories: Object.fromEntries(aggregate.categories)
@@ -28111,7 +28120,41 @@ function createRunTurn(bag) {
28111
28120
  const deliverToolResultMessage = (message) => {
28112
28121
  if (message?.__earlyNotify === true) {
28113
28122
  const earlyCallId = toolResultCallId(message);
28114
- if (earlyCallId) markToolCardCompletedState(earlyCallId, message);
28123
+ if (earlyCallId) {
28124
+ markToolCardCompletedState(earlyCallId, message);
28125
+ const earlyTwin = postShellTwins.get(earlyCallId);
28126
+ if (earlyTwin) markToolCardCompletedState(earlyTwin, message);
28127
+ }
28128
+ return;
28129
+ }
28130
+ const parentCallId = toolResultCallId(message);
28131
+ const twinCallId = parentCallId ? postShellTwins.get(parentCallId) : null;
28132
+ if (twinCallId && !resultsDone.has(twinCallId)) {
28133
+ const combined = String(toolResultText(message?.content) ?? "");
28134
+ const headerMatch = combined.match(/\n+--- post_shell( FAILED[^\n]*)? ---\n?/);
28135
+ const skippedMatch = headerMatch ? null : combined.match(/\n+--- post_shell skipped: patch failed ---\s*$/);
28136
+ let patchPart = combined;
28137
+ let shellPart = null;
28138
+ if (headerMatch) {
28139
+ patchPart = combined.slice(0, headerMatch.index);
28140
+ shellPart = combined.slice(headerMatch.index + headerMatch[0].length);
28141
+ if (headerMatch[1]) shellPart = `Error: post_shell FAILED (patch is applied; fix and re-verify)
28142
+ ${shellPart}`;
28143
+ } else if (skippedMatch) {
28144
+ patchPart = combined.slice(0, skippedMatch.index);
28145
+ shellPart = "Error: post_shell skipped: patch failed";
28146
+ }
28147
+ if (shellPart !== null) {
28148
+ flushToolResults([
28149
+ { role: "tool", tool_call_id: parentCallId, content: patchPart },
28150
+ { role: "tool", tool_call_id: twinCallId, content: shellPart }
28151
+ ], toolCards, cardByCallId, toolGroups, resultsDone);
28152
+ return;
28153
+ }
28154
+ flushToolResults([
28155
+ message,
28156
+ { role: "tool", tool_call_id: twinCallId, content: "(post_shell output included in the patch result above)" }
28157
+ ], toolCards, cardByCallId, toolGroups, resultsDone);
28115
28158
  return;
28116
28159
  }
28117
28160
  flushToolResults([message], toolCards, cardByCallId, toolGroups, resultsDone);
@@ -28154,6 +28197,25 @@ function createRunTurn(bag) {
28154
28197
  }
28155
28198
  const batchCalls = (calls || []).filter(Boolean);
28156
28199
  if (batchCalls.length === 0) return;
28200
+ const displayCalls = [];
28201
+ for (const c of batchCalls) {
28202
+ displayCalls.push(c);
28203
+ if (toolCallName(c) !== "apply_patch") continue;
28204
+ let patchCallArgs = toolCallArgs(c);
28205
+ if (typeof patchCallArgs === "string") {
28206
+ try {
28207
+ patchCallArgs = JSON.parse(patchCallArgs);
28208
+ } catch {
28209
+ patchCallArgs = null;
28210
+ }
28211
+ }
28212
+ const postShellCmd = typeof patchCallArgs?.post_shell === "string" ? patchCallArgs.post_shell.trim() : "";
28213
+ const parentCallId = toolCallId(c);
28214
+ if (!postShellCmd || !parentCallId) continue;
28215
+ const twinCallId = `${parentCallId}::post_shell`;
28216
+ postShellTwins.set(parentCallId, twinCallId);
28217
+ displayCalls.push({ id: twinCallId, name: "shell", args: { command: postShellCmd } });
28218
+ }
28157
28219
  const agentBatch = ++providerToolBatch;
28158
28220
  const committedAssistantSegment = commitAssistantSegment({ sealToolBlock: true });
28159
28221
  if (committedAssistantSegment) {
@@ -28162,11 +28224,14 @@ function createRunTurn(bag) {
28162
28224
  }
28163
28225
  const touchedAggregates = /* @__PURE__ */ new Set();
28164
28226
  let standaloneReserve = null;
28165
- for (let i = 0; i < batchCalls.length; i++) {
28166
- const c = batchCalls[i];
28227
+ let sawEditInBatch = false;
28228
+ for (let i = 0; i < displayCalls.length; i++) {
28229
+ const c = displayCalls[i];
28167
28230
  const name = toolCallName(c);
28168
28231
  const args = toolCallArgs(c);
28169
28232
  const category = classifyToolCategory(name, args);
28233
+ const shellAfterEdit = category === "Shell" && sawEditInBatch;
28234
+ if (category === "Patch" && args?.dry_run !== true) sawEditInBatch = true;
28170
28235
  const bucket = aggregateBucketForCategory(category, { agentBatch });
28171
28236
  const callId = toolCallId(c);
28172
28237
  const callKey = callId || `__tool_${toolCards.length}_${i}`;
@@ -28204,6 +28269,7 @@ function createRunTurn(bag) {
28204
28269
  }
28205
28270
  const categoryEntry = aggregateToolCategoryEntry(name, args, category);
28206
28271
  const aggregateCard = ensureAggregateCard(bucket);
28272
+ if (shellAfterEdit) aggregateCard.verifyShell = true;
28207
28273
  if (!aggregateCard.categories.has(categoryEntry.key)) aggregateCard.categoryOrder.push(categoryEntry.key);
28208
28274
  const prevCategory = aggregateCard.categories.get(categoryEntry.key);
28209
28275
  aggregateCard.categories.set(categoryEntry.key, {
@@ -29956,6 +30022,10 @@ function createEngineApiA(bag) {
29956
30022
  },
29957
30023
  setCwd: (path4, options = {}) => {
29958
30024
  const next = runtime.setCwd(path4);
30025
+ try {
30026
+ touchProjectSelected(next);
30027
+ } catch {
30028
+ }
29959
30029
  const sessionList = recomputePromptHistory(getState().items);
29960
30030
  set({ cwd: next, promptHistoryList: buildMergedPromptHistory(sessionList, loadPromptHistory(next)) });
29961
30031
  if (options?.notice !== false) {
@@ -30659,7 +30729,12 @@ function createLiveShare({
30659
30729
  });
30660
30730
  attachLineReader(socket, (frame) => {
30661
30731
  if (frame.t === "submit" && typeof frame.text === "string" && frame.text.trim()) {
30662
- onRemoteSubmit(frame.text);
30732
+ const id2 = typeof frame.id === "string" && frame.id.trim() ? frame.id : void 0;
30733
+ const submittedAt = Number(frame.submittedAt);
30734
+ onRemoteSubmit(frame.text, {
30735
+ ...id2 ? { id: id2 } : {},
30736
+ ...Number.isFinite(submittedAt) && submittedAt > 0 ? { submittedAt: Math.round(submittedAt) } : {}
30737
+ });
30663
30738
  } else if (frame.t === "abort") {
30664
30739
  onRemoteAbort?.();
30665
30740
  } else if (frame.t === "sync") {
@@ -30960,10 +31035,17 @@ function createLiveShare({
30960
31035
  ensure: ensureShare,
30961
31036
  viewerConnected: () => clientUp,
30962
31037
  waitForViewerSync,
30963
- sendSubmit(text) {
31038
+ sendSubmit(text, meta = null) {
30964
31039
  if (!clientUp || !client) return false;
30965
31040
  try {
30966
- client.write(frameLine({ t: "submit", text: String(text) }));
31041
+ const id = meta && meta.id != null && String(meta.id).trim() ? String(meta.id) : void 0;
31042
+ const submittedAt = Number(meta?.submittedAt);
31043
+ client.write(frameLine({
31044
+ t: "submit",
31045
+ text: String(text),
31046
+ ...id ? { id } : {},
31047
+ ...Number.isFinite(submittedAt) && submittedAt > 0 ? { submittedAt: Math.round(submittedAt) } : {}
31048
+ }));
30967
31049
  return true;
30968
31050
  } catch {
30969
31051
  return false;
@@ -32098,9 +32180,9 @@ async function createEngineSession({
32098
32180
  socketPathFor: (id) => liveSharePipePath(id, sessionPath(id)),
32099
32181
  getPublishedState: () => publishedState,
32100
32182
  listeners,
32101
- onRemoteSubmit: (text) => {
32183
+ onRemoteSubmit: (text, meta) => {
32102
32184
  if (flags.disposed || state.sessionRemoteAttached) return;
32103
- bag.enqueue(text);
32185
+ bag.enqueue(text, meta && meta.id ? { id: meta.id, submittedAt: meta.submittedAt } : {});
32104
32186
  void bag.drain();
32105
32187
  },
32106
32188
  onRemoteAbort: () => {
@@ -32147,7 +32229,7 @@ async function createEngineSession({
32147
32229
  liveShare.ensure();
32148
32230
  } catch {
32149
32231
  }
32150
- if (text && liveShare.sendSubmit(text)) return true;
32232
+ if (text && liveShare.sendSubmit(text, { id: options.id, submittedAt: options.submittedAt })) return true;
32151
32233
  return runtime.enqueueRemoteAttachedPrompt?.(prompt) === true;
32152
32234
  }
32153
32235
  return baseSubmit(prompt, options);
@@ -32176,7 +32258,7 @@ async function createEngineSession({
32176
32258
  reconcileLiveShareNow();
32177
32259
  if (method === "resume" && result === true && state.sessionRemoteAttached) {
32178
32260
  const id = String(state.sessionId || "");
32179
- if (id && await liveShare.waitForViewerSync(id)) bag.flushEmit();
32261
+ if (id && await liveShare.waitForViewerSync(id, 400)) bag.flushEmit();
32180
32262
  }
32181
32263
  return result;
32182
32264
  };
@@ -32694,6 +32776,10 @@ async function runTui({ provider, model, toolMode, remote, forceOnboarding } = {
32694
32776
  const outcome = await storeOutcomePromise;
32695
32777
  if (outcome.error) throw outcome.error;
32696
32778
  store = outcome.store;
32779
+ try {
32780
+ touchProjectSelected(store.getState?.()?.cwd || process.cwd());
32781
+ } catch {
32782
+ }
32697
32783
  bootProfile2("store:ready", { ms: (performance4.now() - startedAt).toFixed(1) });
32698
32784
  } catch (error) {
32699
32785
  splash.stop();
@@ -287,7 +287,18 @@ export function createLiveShare({
287
287
  });
288
288
  attachLineReader(socket, (frame) => {
289
289
  if (frame.t === 'submit' && typeof frame.text === 'string' && frame.text.trim()) {
290
- onRemoteSubmit(frame.text);
290
+ // Optional submission metadata (additive, old viewers omit it): the
291
+ // originating surface's submission id must survive the pipe so its
292
+ // optimistic user row releases when the SAME id settles in the
293
+ // owner's transcript (user: 방금 친 메세지가 2개 남는다).
294
+ const id = typeof frame.id === 'string' && frame.id.trim() ? frame.id : undefined;
295
+ const submittedAt = Number(frame.submittedAt);
296
+ onRemoteSubmit(frame.text, {
297
+ ...(id ? { id } : {}),
298
+ ...(Number.isFinite(submittedAt) && submittedAt > 0
299
+ ? { submittedAt: Math.round(submittedAt) }
300
+ : {}),
301
+ });
291
302
  } else if (frame.t === 'abort') {
292
303
  // Viewer stop button: interrupt the owner's active turn here — the
293
304
  // viewer process has no turn of its own to cancel.
@@ -589,10 +600,19 @@ export function createLiveShare({
589
600
  ensure: ensureShare,
590
601
  viewerConnected: () => clientUp,
591
602
  waitForViewerSync,
592
- sendSubmit(text) {
603
+ sendSubmit(text, meta = null) {
593
604
  if (!clientUp || !client) return false;
594
605
  try {
595
- client.write(frameLine({ t: 'submit', text: String(text) }));
606
+ const id = meta && meta.id != null && String(meta.id).trim() ? String(meta.id) : undefined;
607
+ const submittedAt = Number(meta?.submittedAt);
608
+ client.write(frameLine({
609
+ t: 'submit',
610
+ text: String(text),
611
+ ...(id ? { id } : {}),
612
+ ...(Number.isFinite(submittedAt) && submittedAt > 0
613
+ ? { submittedAt: Math.round(submittedAt) }
614
+ : {}),
615
+ }));
596
616
  return true;
597
617
  } catch {
598
618
  return false;
@@ -8,6 +8,7 @@ import { createEngineApiB } from './session-api-ext.mjs';
8
8
  import { buildDoctorReport } from '../app/doctor.mjs';
9
9
  import { recomputePromptHistory } from './prompt-history.mjs';
10
10
  import { buildMergedPromptHistory, loadPromptHistory } from '../prompt-history-store.mjs';
11
+ import { touchProjectSelected } from '../../standalone/projects.mjs';
11
12
 
12
13
  // Upper bound on how long a manual Esc abort may leave the store busy while the
13
14
  // in-flight runtime.ask() unwinds. runtime.abort() normally rejects the pending
@@ -255,6 +256,12 @@ export function createEngineApiA(bag) {
255
256
  },
256
257
  setCwd: (path, options = {}) => {
257
258
  const next = runtime.setCwd(path);
259
+ // "Last opened" recency belongs to the ACTUAL cwd switch, not to one
260
+ // caller: the project picker, its Current Path row and the /cwd prompt
261
+ // all land here, so the project list stays ordered by what was really
262
+ // opened last. Unregistered folders are ignored — this never registers
263
+ // a project on its own (addProject remains the only registration path).
264
+ try { touchProjectSelected(next); } catch { /* best-effort recency */ }
258
265
  // Republish up-arrow history for the NEW project: current session prompts
259
266
  // merged with the cwd-scoped persisted store for the new cwd.
260
267
  const sessionList = recomputePromptHistory(getState().items);