mixdog 0.9.93 → 0.9.95

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 (186) hide show
  1. package/NOTICE.md +72 -0
  2. package/package.json +16 -6
  3. package/scripts/lib/isolated-root-cleanup.mjs +19 -0
  4. package/scripts/run-suite.mjs +100 -0
  5. package/src/lib/rules-builder.cjs +4 -3
  6. package/src/output-styles/detailed.md +13 -15
  7. package/src/output-styles/extreme-minimal.md +7 -11
  8. package/src/output-styles/minimal.md +4 -7
  9. package/src/output-styles/simple.md +11 -13
  10. package/src/rules/agent/30-explorer.md +22 -16
  11. package/src/rules/lead/01-general.md +1 -2
  12. package/src/rules/lead/lead-brief.md +4 -5
  13. package/src/rules/lead/lead-tool.md +3 -2
  14. package/src/rules/shared/01-tool.md +28 -29
  15. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
  16. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +23 -7
  17. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
  18. package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
  19. package/src/runtime/agent/orchestrator/context/collect.mjs +6 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -3
  21. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
  22. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
  23. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  25. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
  26. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
  27. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
  28. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
  29. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -8
  30. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
  31. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
  32. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
  33. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +10 -10
  34. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
  35. package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
  36. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -15
  37. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +12 -32
  38. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
  39. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
  40. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
  41. package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
  42. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -0
  43. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
  44. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
  45. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
  46. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +7 -4
  47. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
  49. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +40 -13
  50. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
  51. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
  52. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +5 -9
  53. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +3 -3
  54. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +103 -92
  55. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
  56. package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -3
  57. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
  58. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +25 -3
  59. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
  60. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  61. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +12 -3
  62. package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +22 -0
  63. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +491 -0
  64. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +91 -13
  65. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +90 -27
  66. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +20 -1
  67. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
  69. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
  70. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
  71. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
  72. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
  73. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +7 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +32 -4
  75. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +1 -1
  76. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +16 -1
  77. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +546 -23
  78. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +8 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +12 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -3
  81. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +35 -5
  82. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
  83. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
  84. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +48 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin.mjs +71 -1
  86. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +7 -3
  87. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +104 -14
  88. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
  89. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +6 -17
  90. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -4
  91. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
  92. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
  93. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +5 -5
  94. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
  95. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
  96. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +151 -18
  97. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +5 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +109 -13
  99. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
  100. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -5
  101. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -0
  102. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
  103. package/src/runtime/channels/backends/discord.mjs +5 -14
  104. package/src/runtime/channels/backends/telegram.mjs +0 -5
  105. package/src/runtime/channels/lib/inbound-handler.mjs +0 -1
  106. package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
  107. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  108. package/src/runtime/channels/lib/worker-main.mjs +1 -1
  109. package/src/runtime/media/renditions.mjs +21 -2
  110. package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
  111. package/src/runtime/memory/tool-defs.mjs +6 -6
  112. package/src/runtime/shared/atomic-file.mjs +53 -0
  113. package/src/runtime/shared/background-tasks.mjs +16 -6
  114. package/src/runtime/shared/child-spawn-gate.mjs +50 -26
  115. package/src/runtime/shared/resource-admission.mjs +40 -1
  116. package/src/runtime/shared/task-notification-envelope.mjs +11 -2
  117. package/src/runtime/shared/tool-card-model.mjs +6 -2
  118. package/src/runtime/shared/tool-status.mjs +10 -1
  119. package/src/runtime/shared/tool-surface.mjs +5 -0
  120. package/src/runtime/shared/turn-snapshot.mjs +385 -21
  121. package/src/runtime/shared/turn-worktree-snapshot.mjs +540 -0
  122. package/src/session-runtime/context-status.mjs +9 -3
  123. package/src/session-runtime/lifecycle-api.mjs +35 -5
  124. package/src/session-runtime/mcp-glue.mjs +11 -6
  125. package/src/session-runtime/provider-models.mjs +94 -43
  126. package/src/session-runtime/provider-usage.mjs +26 -2
  127. package/src/session-runtime/runtime-core.mjs +46 -8
  128. package/src/session-runtime/runtime-tunables.mjs +4 -0
  129. package/src/session-runtime/self-update.mjs +33 -4
  130. package/src/session-runtime/session-lifecycle.mjs +2 -0
  131. package/src/session-runtime/session-text.mjs +2 -1
  132. package/src/session-runtime/session-turn-api.mjs +106 -22
  133. package/src/session-runtime/workflow.mjs +11 -4
  134. package/src/standalone/agent-tool.mjs +4 -4
  135. package/src/standalone/backend-daemon.mjs +570 -0
  136. package/src/standalone/channel-daemon-transport.mjs +141 -1
  137. package/src/standalone/channel-worker.mjs +3 -2
  138. package/src/standalone/engine-daemon-client.mjs +894 -0
  139. package/src/standalone/engine-daemon-local-bridge.mjs +20 -0
  140. package/src/standalone/engine-daemon-protocol.mjs +33 -0
  141. package/src/standalone/engine-daemon-service.mjs +864 -0
  142. package/src/standalone/engine-daemon-transport.mjs +603 -0
  143. package/src/standalone/explore-tool.mjs +1 -1
  144. package/src/tui/App.jsx +62 -47
  145. package/src/tui/app/app-format.mjs +4 -2
  146. package/src/tui/app/app-view.jsx +5 -0
  147. package/src/tui/app/channel-pickers.mjs +7 -6
  148. package/src/tui/app/core-memory-picker.mjs +4 -4
  149. package/src/tui/app/extension-pickers.mjs +20 -18
  150. package/src/tui/app/maintenance-pickers.mjs +27 -27
  151. package/src/tui/app/onboarding-steps.mjs +23 -18
  152. package/src/tui/app/prompt-submit.mjs +13 -2
  153. package/src/tui/app/route-pickers.mjs +13 -8
  154. package/src/tui/app/settings-picker.mjs +55 -58
  155. package/src/tui/app/slash-dispatch.mjs +32 -28
  156. package/src/tui/app/transcript-window.mjs +19 -0
  157. package/src/tui/app/usage-context-panels.mjs +22 -7
  158. package/src/tui/app/use-mouse-input.mjs +25 -3
  159. package/src/tui/app/use-prompt-queue-history.mjs +31 -16
  160. package/src/tui/app/use-transcript-scroll.mjs +30 -8
  161. package/src/tui/app/use-transcript-window.mjs +22 -1
  162. package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
  163. package/src/tui/components/PromptInput.jsx +10 -0
  164. package/src/tui/components/Spinner.jsx +89 -86
  165. package/src/tui/components/TextEntryPanel.jsx +14 -0
  166. package/src/tui/components/ToolExecution.jsx +2 -2
  167. package/src/tui/dist/index.mjs +1537 -9718
  168. package/src/tui/engine/agent-job-feed.mjs +2 -2
  169. package/src/tui/engine/live-share.mjs +97 -4
  170. package/src/tui/engine/session-api-ext.mjs +23 -1
  171. package/src/tui/engine/session-api.mjs +88 -41
  172. package/src/tui/engine/session-flow.mjs +43 -4
  173. package/src/tui/engine/tool-card-results.mjs +6 -0
  174. package/src/tui/engine/turn.mjs +84 -4
  175. package/src/tui/engine-local-session.mjs +1108 -0
  176. package/src/tui/engine.mjs +16 -1057
  177. package/src/tui/index.jsx +47 -2
  178. package/src/tui/markdown/stream-fence.mjs +1 -1
  179. package/src/tui/spinner-meta.mjs +80 -0
  180. package/src/tui/spinner-verbs.mjs +35 -0
  181. package/src/ui/statusline-segments.mjs +43 -10
  182. package/src/ui/statusline.mjs +10 -1
  183. package/scripts/tmp-cdp-errors.mjs +0 -41
  184. package/scripts/tmp-cdp-inspect.mjs +0 -41
  185. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
  186. package/src/standalone/channel-daemon.mjs +0 -226
package/src/tui/index.jsx CHANGED
@@ -18,11 +18,19 @@ import { scheduleRenderFrameAck, TUI_RENDER_FPS } from './engine/render-timing.m
18
18
  import { installProcessSignalCleanup } from '../runtime/shared/process-shutdown.mjs';
19
19
  import { finishProcessLifecycle } from '../runtime/shared/process-lifecycle.mjs';
20
20
  import { rgbSgr } from '../ui/ansi.mjs';
21
- import { emitTerminalBackground, loadThemeSettingFromConfig, theme } from './theme.mjs';
21
+ import {
22
+ emitTerminalBackground,
23
+ getThemeSetting,
24
+ listThemes,
25
+ loadThemeSettingFromConfig,
26
+ setThemeSetting,
27
+ theme,
28
+ } from './theme.mjs';
22
29
  import { POP_KITTY, DISABLE_MODIFY_OTHER_KEYS } from './keyboard-protocol.mjs';
23
30
  import { displayWidth } from './display-width.mjs';
24
31
  import { rotateBoundedLog, PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES } from '../lib/mixdog-debug.cjs';
25
32
  import { localPackageVersion } from '../runtime/shared/update-checker.mjs';
33
+ import { touchProjectSelected } from '../standalone/projects.mjs';
26
34
 
27
35
  // Trailing `\x1b[>0s` restores XTSHIFTESCAPE (shift-to-select-extend) to its
28
36
  // terminal default; MOUSE_TRACKING_ON opts into `\x1b[>1s`, so every mouse/alt
@@ -505,6 +513,29 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
505
513
  const outcome = await storeOutcomePromise;
506
514
  if (outcome.error) throw outcome.error;
507
515
  store = outcome.store;
516
+ // Launching mixdog INSIDE a registered folder is an "open" too: without
517
+ // this, a project entered straight from the shell never moved in the
518
+ // picker's most-recent-first order. Unregistered folders are ignored.
519
+ try {
520
+ touchProjectSelected(store.getState?.()?.cwd || process.cwd());
521
+ } catch { /* best-effort recency */ }
522
+ // The palette lives in THIS process: on a daemon-backed store the engine's
523
+ // theme methods would run inside the daemon, so the picker saw a promise
524
+ // instead of the list and a switch repainted nothing here. Bind the three
525
+ // theme methods locally (same pattern as the ink selection helpers below)
526
+ // and mirror the switch to the engine with persist:false, purely so its
527
+ // themeEpoch bump re-renders this tree.
528
+ if (store.isRemoteEngine === true) {
529
+ const remoteSetTheme = store.setTheme;
530
+ store.listThemes = () => listThemes();
531
+ store.getTheme = () => getThemeSetting();
532
+ store.setTheme = (id, options = {}) => {
533
+ const applied = setThemeSetting(id, options);
534
+ void Promise.resolve(remoteSetTheme?.(id, { ...options, persist: false }))
535
+ .catch(() => { /* the local palette is already applied */ });
536
+ return applied;
537
+ };
538
+ }
508
539
  bootProfile('store:ready', { ms: (performance.now() - startedAt).toFixed(1) });
509
540
  } catch (error) {
510
541
  splash.stop();
@@ -517,6 +548,20 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
517
548
  process.stderr.write(`mixdog: ${error?.message || error}\n`);
518
549
  return 1;
519
550
  }
551
+ // The engine store is a daemon-backed Proxy: every method it does not
552
+ // implement locally comes back as an ASYNC remote call. A synchronous
553
+ // getOnboardingStatus() probe inside App therefore sees a Promise, never
554
+ // `.completed`, and re-opens the finished wizard on every launch. Resolve
555
+ // the status once here and hand the decided verdict to App. An unreadable
556
+ // status counts as completed so a transport hiccup cannot resurrect a
557
+ // wizard the user already dismissed (`mixdog --onboarding` still forces it).
558
+ let onboardingCompleted = true;
559
+ try {
560
+ const onboardingStatus = await store.getOnboardingStatus?.();
561
+ if (onboardingStatus && typeof onboardingStatus === 'object') {
562
+ onboardingCompleted = onboardingStatus.completed === true;
563
+ }
564
+ } catch { /* status probe failed → treat as completed */ }
520
565
  // Stop the spinner BEFORE ink mounts so no stray splash write can land
521
566
  // between (or after) ink's first frames.
522
567
  splash.stop();
@@ -598,7 +643,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
598
643
  // its clear-frame → write → relative re-render dance, which scrolls the
599
644
  // alt screen one line per stray console line (the streaming newline
600
645
  // bounce). See installTuiConsoleGuard.
601
- const instance = render(<App store={store} forceOnboarding={forceOnboarding === true} />, { exitOnCtrlC: false, maxFps: TUI_RENDER_FPS, incrementalRendering: true, patchConsole: false, onRender: makeRenderProfiler() });
646
+ const instance = render(<App store={store} forceOnboarding={forceOnboarding === true} onboardingCompleted={onboardingCompleted} />, { exitOnCtrlC: false, maxFps: TUI_RENDER_FPS, incrementalRendering: true, patchConsole: false, onRender: makeRenderProfiler() });
602
647
  bootProfile('render:mounted', { ms: (performance.now() - startedAt).toFixed(1) });
603
648
  const { waitUntilExit } = instance;
604
649
  // [mixdog fork] Hand the ink renderer's drag-selection setter to the store so
@@ -9,7 +9,7 @@
9
9
  * so the block height stays stable across deltas.
10
10
  *
11
11
  * Kept in its own dependency-free module so it can be unit-tested without the
12
- * ink/JSX render stack. Ref: earendil-works/pi packages/tui/src/components/markdown.ts.
12
+ * ink/JSX render stack.
13
13
  */
14
14
  export function trimPartialClosingFences(tokens) {
15
15
  const token = tokens?.[tokens.length - 1];
@@ -0,0 +1,80 @@
1
+ /**
2
+ * src/tui/spinner-meta.mjs — the spinner BYLINE contract, shared by the TUI
3
+ * spinner row and the desktop live-activity band.
4
+ *
5
+ * The verb pool already lives in spinner-verbs.mjs so both surfaces say the
6
+ * same word at the same second. Everything AFTER the verb (elapsed gate, token
7
+ * label, thinking / "thought for Ns") used to be assembled twice, which is how
8
+ * the desktop ended up showing tokens from second one while the TUI hid them
9
+ * for 30s. One builder, one answer.
10
+ */
11
+
12
+ /** Short turns stay quiet: elapsed/token meta only after this long. */
13
+ export const SHOW_TOKENS_AFTER_MS = 30_000;
14
+
15
+ const compactNumberFormatter = new Intl.NumberFormat('en-US', {
16
+ notation: 'compact',
17
+ maximumFractionDigits: 1,
18
+ minimumFractionDigits: 0,
19
+ });
20
+
21
+ export function formatSpinnerTokens(value) {
22
+ const tokens = Math.max(0, Number(value || 0));
23
+ if (tokens >= 1000) return compactNumberFormatter.format(tokens).toLowerCase();
24
+ return String(Math.round(tokens));
25
+ }
26
+
27
+ // `auto` is the absence of a choice, so it never becomes a visible suffix —
28
+ // only an explicitly picked level earns "thinking (high)".
29
+ function effortSuffix(effort) {
30
+ const level = String(effort || '').trim().toLowerCase();
31
+ if (!level || level === 'auto' || level === 'default' || level === 'off' || level === 'none') return '';
32
+ return ` (${level})`;
33
+ }
34
+
35
+ /**
36
+ * Live thinking reads as `thinking`; a finished thinking span reads as
37
+ * `thought for Ns` until the next one starts. Sub-second
38
+ * spans are noise and stay hidden.
39
+ */
40
+ export function spinnerThinkingLabel({ thinking = false, thinkingSince = 0, thinkingMs = 0, effort = '' } = {}) {
41
+ if (thinking || Number(thinkingSince) > 0) return `thinking${effortSuffix(effort)}`;
42
+ const ms = Math.max(0, Number(thinkingMs) || 0);
43
+ if (ms >= 1000) return `thought for ${Math.max(1, Math.round(ms / 1000))}s`;
44
+ return '';
45
+ }
46
+
47
+ /**
48
+ * Byline pieces for one live turn. Width gating stays with each surface (the
49
+ * TUI measures terminal columns, the desktop wraps in CSS) — this only decides
50
+ * WHAT is worth showing and how it reads.
51
+ */
52
+ export function buildSpinnerMeta({
53
+ elapsedMs = 0,
54
+ outputTokens = 0,
55
+ thinking = false,
56
+ thinkingSince = 0,
57
+ thinkingMs = 0,
58
+ effort = '',
59
+ verbose = false,
60
+ } = {}) {
61
+ const tokens = Math.max(0, Number(outputTokens) || 0);
62
+ const thinkingText = spinnerThinkingLabel({ thinking, thinkingSince, thinkingMs, effort });
63
+ return {
64
+ tokensText: tokens > 0 ? `${formatSpinnerTokens(tokens)} tokens` : '',
65
+ showTokens: tokens > 0 && (Boolean(verbose) || Number(elapsedMs) > SHOW_TOKENS_AFTER_MS),
66
+ thinkingText,
67
+ thinkingActive: Boolean(thinking || Number(thinkingSince) > 0),
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Reduced motion: one switch for the whole process — animation is a nicety,
73
+ * and low-power/SSH/recording sessions must be able to drop it without losing
74
+ * the timer or the token readout.
75
+ */
76
+ export function isReducedMotion(env) {
77
+ const source = env || (typeof process !== 'undefined' && process?.env) || {};
78
+ const value = String(source.MIXDOG_REDUCED_MOTION ?? '').trim().toLowerCase();
79
+ return value === '1' || value === 'true' || value === 'on' || value === 'yes';
80
+ }
@@ -2,6 +2,11 @@
2
2
  * src/tui/spinner-verbs.mjs — playful "thinking" verbs for the spinner.
3
3
  *
4
4
  * Playful spinner verbs. Shown as `<Verb>… (Ns · ↑ N tokens)` while a turn runs.
5
+ *
6
+ * ONE common pool, never a per-mode pool: the stream flips
7
+ * thinking → tool-use → responding many times inside a single turn, so a
8
+ * mode-keyed phrase rewrites itself every few seconds and reads as flicker.
9
+ * The phrase is drawn from this list and held for a fixed window instead.
5
10
  */
6
11
  export const SPINNER_VERBS = [
7
12
  'Accomplishing', 'Actioning', 'Actualizing', 'Architecting', 'Baking', 'Beaming',
@@ -40,5 +45,35 @@ export const SPINNER_VERBS = [
40
45
  'Zesting', 'Zigzagging',
41
46
  ];
42
47
 
48
+ /** How long one phrase holds before the pool advances. */
49
+ export const SPINNER_VERB_ROTATE_MS = 30_000;
50
+
51
+ /**
52
+ * Modes that describe a STATE rather than ongoing work: they replace the pool
53
+ * phrase for as long as the mode lasts, then the rotation resumes. Everything
54
+ * else (requesting/thinking/tool-use/tool-input/responding) shares the pool.
55
+ * `reconnecting` is absent on purpose — the engine authors that text itself
56
+ * (retry countdown), so the surfaces pass `spinner.verb` through unchanged.
57
+ */
58
+ export const SPINNER_MODE_OVERRIDE_VERBS = {
59
+ compacting: 'Compacting conversation',
60
+ 'auto-clear': 'Auto-clearing conversation',
61
+ resuming: 'Resuming conversation',
62
+ };
63
+
64
+ /**
65
+ * The phrase for a turn at a point in time. Derived from the turn's
66
+ * `startedAt` so the TUI and the desktop show the SAME word at the same
67
+ * second, and so mode flips cannot change it — only the 30s window can.
68
+ */
69
+ export function spinnerVerbFor(startedAt = 0, now = 0, pool = SPINNER_VERBS) {
70
+ const list = Array.isArray(pool) && pool.length ? pool : SPINNER_VERBS;
71
+ const anchor = Math.max(0, Number(startedAt) || 0);
72
+ const elapsed = anchor > 0 ? Math.max(0, (Number(now) || 0) - anchor) : 0;
73
+ const slot = Math.floor(elapsed / SPINNER_VERB_ROTATE_MS);
74
+ const seed = Math.floor(anchor / 1000) + slot;
75
+ return list[((seed * 7) + 3) % list.length];
76
+ }
77
+
43
78
  /** Spinner glyph frames. */
44
79
  export const SPINNER_FRAMES = ['◇', '◆', '◈', '◆', '◇'];
@@ -18,7 +18,13 @@ const DEFAULT_MIXDOG_HOME = process.env.MIXDOG_HOME || join(homedir(), '.mixdog'
18
18
  const DEFAULT_STANDALONE_DATA_DIR = join(DEFAULT_MIXDOG_HOME, 'data');
19
19
  const SHELL_JOBS_SEGMENT_CACHE_MS = 1000;
20
20
 
21
- let _shellJobsSegmentCache = { ownerPid: 0, at: 0, value: { count: 0, elapsedLabel: '' } };
21
+ // Session buckets ride along with the owner-wide totals: one host process can
22
+ // own many sessions (the desktop pools every pane's engine), so a pane must be
23
+ // able to ask for ITS OWN jobs instead of the process aggregate.
24
+ const EMPTY_SHELL_JOBS_SESSION = Object.freeze({ count: 0, elapsedLabel: '' });
25
+ const EMPTY_SHELL_JOBS = Object.freeze({ count: 0, elapsedLabel: '', sessions: Object.freeze({}) });
26
+
27
+ let _shellJobsSegmentCache = { ownerPid: 0, at: 0, value: EMPTY_SHELL_JOBS };
22
28
  let _shellJobsRefreshInFlight = false;
23
29
 
24
30
  function dataDir() {
@@ -26,10 +32,16 @@ function dataDir() {
26
32
  }
27
33
 
28
34
  // Render-path entry point: synchronous, cache-only. Never blocks on fs.
29
- export function shellJobsStatus({ clientHostPid } = {}) {
35
+ // Without `sessionId` this reports the owner process aggregate (CLI statusline,
36
+ // keep-awake); with one it reports only that session's own jobs.
37
+ export function shellJobsStatus({ clientHostPid, sessionId } = {}) {
30
38
  const ownerPid = positiveInt(clientHostPid);
31
- const empty = { count: 0, elapsedLabel: '' };
32
- if (!ownerPid) return empty;
39
+ // Asking for a scope is what matters, not whether that scope is non-empty:
40
+ // an explicit blank id (a desktop pane with no session yet) owns nothing,
41
+ // while an OMITTED id means "the whole owner process".
42
+ const scoped = sessionId !== undefined && sessionId !== null;
43
+ const scope = String(sessionId ?? '').trim();
44
+ if (!ownerPid) return scoped ? EMPTY_SHELL_JOBS_SESSION : EMPTY_SHELL_JOBS;
33
45
  const now = Date.now();
34
46
  const sameOwner = _shellJobsSegmentCache.ownerPid === ownerPid;
35
47
  const fresh = sameOwner && now - _shellJobsSegmentCache.at < SHELL_JOBS_SEGMENT_CACHE_MS;
@@ -37,7 +49,9 @@ export function shellJobsStatus({ clientHostPid } = {}) {
37
49
  _shellJobsRefreshInFlight = true;
38
50
  refreshShellJobsStatus(ownerPid).finally(() => { _shellJobsRefreshInFlight = false; });
39
51
  }
40
- return sameOwner ? (_shellJobsSegmentCache.value || empty) : empty;
52
+ const value = sameOwner ? (_shellJobsSegmentCache.value || EMPTY_SHELL_JOBS) : EMPTY_SHELL_JOBS;
53
+ if (!scoped) return value;
54
+ return (scope && value.sessions?.[scope]) || EMPTY_SHELL_JOBS_SESSION;
41
55
  }
42
56
 
43
57
  // Memory cycle L2 segment source. The memory daemon writes
@@ -95,8 +109,7 @@ function pidAlive(pid) {
95
109
  }
96
110
 
97
111
  async function refreshShellJobsStatus(ownerPid) {
98
- const empty = { count: 0, elapsedLabel: '' };
99
- let value = empty;
112
+ let value = EMPTY_SHELL_JOBS;
100
113
  try {
101
114
  const dir = join(dataDir(), 'shell-jobs');
102
115
  let names;
@@ -123,23 +136,43 @@ async function refreshShellJobsStatus(ownerPid) {
123
136
  .slice(0, 30);
124
137
  let count = 0;
125
138
  let oldestMs = Infinity;
139
+ // sessionId -> { count, oldestMs }. Jobs with no session stamp (legacy
140
+ // records, plain CLI runs) still count toward the owner total but belong to
141
+ // no pane, so they never light up a session's indicator.
142
+ const bySession = new Map();
126
143
  for (const id of ids) {
127
144
  const p = join(dir, `${id}.json`);
128
145
  let detail;
129
146
  try { detail = JSON.parse(await readFile(p, 'utf-8')); } catch { continue; }
130
147
  if (!(await isShellJobAlive(detail, p, dir, id))) continue;
131
148
  count++;
149
+ let jobMs = Infinity;
132
150
  try {
133
151
  const st = await stat(p);
152
+ jobMs = st.mtimeMs;
134
153
  if (st.mtimeMs < oldestMs) oldestMs = st.mtimeMs;
135
154
  } catch {}
155
+ const owner = String(detail?.ownerSessionId ?? '').trim();
156
+ if (!owner) continue;
157
+ const bucket = bySession.get(owner) || { count: 0, oldestMs: Infinity };
158
+ bucket.count += 1;
159
+ if (jobMs < bucket.oldestMs) bucket.oldestMs = jobMs;
160
+ bySession.set(owner, bucket);
136
161
  }
137
162
  if (count) {
138
- const elapsedLabel = Number.isFinite(oldestMs) ? formatElapsed(Date.now() - oldestMs) : '';
139
- value = { count, elapsedLabel };
163
+ const now = Date.now();
164
+ const elapsedLabel = Number.isFinite(oldestMs) ? formatElapsed(now - oldestMs) : '';
165
+ const sessions = {};
166
+ for (const [owner, bucket] of bySession) {
167
+ sessions[owner] = {
168
+ count: bucket.count,
169
+ elapsedLabel: Number.isFinite(bucket.oldestMs) ? formatElapsed(now - bucket.oldestMs) : '',
170
+ };
171
+ }
172
+ value = { count, elapsedLabel, sessions };
140
173
  }
141
174
  } catch {
142
- value = empty;
175
+ value = EMPTY_SHELL_JOBS;
143
176
  }
144
177
  _shellJobsSegmentCache = { ownerPid, at: Date.now(), value };
145
178
  }
@@ -350,7 +350,16 @@ function renderNativeStatusline({
350
350
  ...activeHiddenAgentWorkers({ sessionId, clientHostPid }),
351
351
  ], agentJobs);
352
352
  const { runningWorkers } = classifyAgentWorkers(agentPayload.workers);
353
- const shellStatus = shellJobsStatus({ clientHostPid });
353
+ // Shell segment scope: one host process can own MANY sessions' jobs (the
354
+ // desktop pools every pane's engine; the daemon backend hosts every
355
+ // terminal's session), so the owner-wide aggregate would show one
356
+ // terminal's running shell on every other terminal. Render this session's
357
+ // own jobs whenever we know which session we are; only a session-less
358
+ // caller (plain statusline shim) keeps the process aggregate.
359
+ const shellScope = String(sessionId ?? '').trim();
360
+ const shellStatus = shellScope
361
+ ? shellJobsStatus({ clientHostPid, sessionId: shellScope })
362
+ : shellJobsStatus({ clientHostPid });
354
363
 
355
364
  const spinnerNow = Date.now();
356
365
  const sp = l2SpinnerFrame(spinnerNow);
@@ -1,41 +0,0 @@
1
- import WebSocket from "ws";
2
-
3
- const list = await fetch("http://127.0.0.1:9342/json/list").then((r) => r.json());
4
- const page = list.find((t) => t.type === "page");
5
- const ws = new WebSocket(page.webSocketDebuggerUrl);
6
- let id = 0; const pending = new Map();
7
- function send(method, params) {
8
- return new Promise((res) => { const mid = ++id; pending.set(mid, res); ws.send(JSON.stringify({ id: mid, method, params })); });
9
- }
10
- const seen = new Map();
11
- ws.on("message", (raw) => {
12
- const msg = JSON.parse(raw);
13
- if (msg.id && pending.has(msg.id)) { pending.get(msg.id)(msg.result); pending.delete(msg.id); return; }
14
- if (msg.method === "Runtime.exceptionThrown") {
15
- const d = msg.params.exceptionDetails;
16
- const desc = d.exception?.description || d.text || "";
17
- const key = desc.slice(0, 200);
18
- seen.set(key, (seen.get(key) || 0) + 1);
19
- } else if (msg.method === "Runtime.consoleAPICalled" && (msg.params.type === "error" || msg.params.type === "warning")) {
20
- const text = msg.params.args.map((a) => a.value ?? a.description ?? "").join(" ");
21
- const key = "[console." + msg.params.type + "] " + text.slice(0, 300);
22
- seen.set(key, (seen.get(key) || 0) + 1);
23
- } else if (msg.method === "Log.entryAdded") {
24
- const e = msg.params.entry;
25
- if (e.level === "error" || e.level === "warning") {
26
- const key = "[log." + e.level + "] " + (e.text || "").slice(0, 300) + " @" + (e.url || "");
27
- seen.set(key, (seen.get(key) || 0) + 1);
28
- }
29
- }
30
- });
31
- ws.on("open", async () => {
32
- await send("Runtime.enable", {});
33
- await send("Log.enable", {});
34
- setTimeout(() => {
35
- for (const [key, count] of [...seen.entries()].sort((a, b) => b[1] - a[1])) {
36
- console.log(`x${count} ${key.replace(/\n/g, " | ")}`);
37
- }
38
- if (!seen.size) console.log("(no errors captured in window)");
39
- process.exit(0);
40
- }, 12000);
41
- });
@@ -1,41 +0,0 @@
1
- import WebSocket from "ws";
2
-
3
- const list = await fetch("http://127.0.0.1:9342/json/list").then((r) => r.json());
4
- const page = list.find((t) => t.type === "page");
5
- if (!page) { console.error("no page target"); process.exit(1); }
6
- const ws = new WebSocket(page.webSocketDebuggerUrl);
7
- let id = 0; const pending = new Map();
8
- function send(method, params) {
9
- return new Promise((res) => { const mid = ++id; pending.set(mid, res); ws.send(JSON.stringify({ id: mid, method, params })); });
10
- }
11
- ws.on("message", (raw) => {
12
- const msg = JSON.parse(raw);
13
- if (msg.id && pending.has(msg.id)) { pending.get(msg.id)(msg.result); pending.delete(msg.id); }
14
- });
15
- ws.on("open", async () => {
16
- const expr = `(() => {
17
- const gate = document.querySelector('.desktop-boot-gate');
18
- const covers = [...document.querySelectorAll('.pane-surface-cover, .desktop-boot-cover')].map(c => ({
19
- cls: c.className,
20
- rect: Math.round(c.getBoundingClientRect().width) + 'x' + Math.round(c.getBoundingClientRect().height),
21
- parent: (c.parentElement?.className || '').slice(0, 90),
22
- hasSpinner: !!c.querySelector('.desktop-loading-spinner'),
23
- }));
24
- const gates = [...document.querySelectorAll('.pane-surface-gate[data-ready="false"], .stable-surface-switch[data-ready="false"], .stable-content-swap[data-ready="false"]')]
25
- .map(g => g.className + ' | parent=' + (g.parentElement?.className || '').slice(0, 70));
26
- return JSON.stringify({
27
- bootGate: gate ? { ready: gate.dataset.ready, pending: gate.dataset.pending, timeout: gate.dataset.timeout } : null,
28
- revealed: window.__mixdogDesktopRevealed,
29
- shown: window.__mixdogWindowShown,
30
- covers, gates,
31
- metrics: (window.__mixdogBootMetrics || []).slice(-30),
32
- recovery: !!document.querySelector('.desktop-recovery-screen'),
33
- rootKids: document.getElementById('root')?.childElementCount,
34
- url: location.href,
35
- }, null, 1);
36
- })()`;
37
- const r = await send("Runtime.evaluate", { expression: expr, returnByValue: true });
38
- console.log(r?.result?.value ?? JSON.stringify(r));
39
- process.exit(0);
40
- });
41
- setTimeout(() => { console.error("timeout"); process.exit(1); }, 8000);
@@ -1,176 +0,0 @@
1
- // Completion-first steering ladder (worker runaway prevention), extracted from
2
- // loop.mjs. Owns the mutable ladder counters and the post-batch steering-hint
3
- // emitters. State is threaded live via a context object of getters/setters so
4
- // no snapshot goes stale — the loop mutates `messages`/`iterations` in place and
5
- // this module reads them through the accessors on each call. No behavior change:
6
- // the counters, thresholds, and emitted messages are verbatim from agentLoop.
7
- import { appendAgentTrace } from '../../agent-trace.mjs';
8
- import { level2SteerMessage } from './completion-guards.mjs';
9
- import { isEagerDispatchable } from './tool-helpers.mjs';
10
-
11
- // A single chained segment is read-only only if it STARTS with a read-only verb.
12
- function _isReadOnlyShellSegment(seg) {
13
- const s = seg.trim();
14
- if (!s) return null; // empty (trailing operator)
15
- if (/^git\s+(status|log)\b/.test(s)) return true;
16
- if (/^git\s+diff\b[^\n]*--stat\b/.test(s)) return true;
17
- if (/^get-childitem\b/.test(s)) return true;
18
- return /^(cat|ls|dir|type)\b/.test(s);
19
- }
20
- // Read-only only when EVERY chained segment is read-only — a single mutating
21
- // segment (e.g. `git status && npm test`) disqualifies the whole command.
22
- function _isReadOnlyShellCmd(cmd) {
23
- const s = String(cmd || '').toLowerCase().trim();
24
- if (!s) return false;
25
- let sawCmd = false;
26
- for (const seg of s.split(/&&|\|\||[;&|]/)) {
27
- const verdict = _isReadOnlyShellSegment(seg);
28
- if (verdict === null) continue; // blank segment
29
- if (verdict === false) return false; // a mutating segment
30
- sawCmd = true;
31
- }
32
- return sawCmd;
33
- }
34
- // Build the completion-first steering-ladder controller. `ctx` supplies live
35
- // accessors so every read reflects the loop's current mutable state:
36
- // - messages, sessionId, sessionAgent, tools (stable refs/values)
37
- // - getIterations() (current iteration)
38
- // - getEditCount() (mutated by the loop)
39
- // - pushSystemReminder(text) → push a meta:'hook' user message
40
- // - pushUserMessage(msg) → push a raw user message (level-2 latch text)
41
- export function createSteeringLadder(ctx) {
42
- const {
43
- sessionId,
44
- sessionAgent,
45
- tools,
46
- getIterations,
47
- getEditCount,
48
- } = ctx;
49
- const pushSystemReminder = ctx.pushSystemReminder;
50
- const pushUserMessage = ctx.pushUserMessage;
51
- // Permission-based role detection (agent names are user-definable):
52
- // read-permission sessions legitimately never edit, so they get the
53
- // report-oriented level-2 text.
54
- const readOnlyRole = ctx.readOnlyRole === true;
55
- // Edit-push steering is EXPLORER-ONLY: leads legitimately read broadly
56
- // before delegating, and reviewer/debugger-style roles read continuously
57
- // by design. Pushing "start editing / apply the edit" at those roles
58
- // suppresses delegation (observed on TB2.1: forced-delegation workflow
59
- // leads went solo right after these nudges). Non-explorer roles keep the
60
- // batching guidance but never receive an edit directive, and level-2
61
- // edit-push is skipped for them entirely.
62
- const editPushEligible = sessionAgent === 'explorer';
63
-
64
- // Step 1: escalation ladder. _level1FireCount is CUMULATIVE (never reset)
65
- // so repeated batching reminders accumulate across the whole session.
66
- // _level2LatchAtIteration latches level-2 steering to at most once / 5 turns.
67
- let _level1FireCount = 0;
68
- let _level2LatchAtIteration = -Infinity;
69
- // Independent ladder counter: consecutive turns where EVERY call is
70
- // read-only (any count) with zero edits. Catches multi-call read-only
71
- // turns that the single-call level-1 streak misses. Reset on any edit.
72
- let _allReadOnlyStreak = 0;
73
- // Tracks consecutive assistant turns that ran exactly one read-only tool
74
- // call (missed parallelism). Not reset per-iteration — only by the
75
- // steering-hint fire below or by a turn that batches/edits.
76
- let _serialReadOnlyStreak = 0;
77
- let _level2FireCount = 0;
78
-
79
- // Level-2 steering emitter shared by both ladder paths (single-call
80
- // level-1 streak and the independent all-read-only streak). Sets the latch
81
- // so it fires at most once per 5 turns regardless of which path triggered.
82
- const _emitLevel2Steer = () => {
83
- if (!editPushEligible && !readOnlyRole) return;
84
- const iterations = getIterations();
85
- _level2LatchAtIteration = iterations;
86
- _level2FireCount += 1;
87
- pushUserMessage({ role: 'user', content: level2SteerMessage(_level1FireCount, readOnlyRole, _level2FireCount), meta: 'hook' });
88
- try {
89
- appendAgentTrace({
90
- sessionId,
91
- iteration: iterations,
92
- kind: 'steer',
93
- payload: { tag: 'level2_steer', level1_fires: _level1FireCount, level2_fires: _level2FireCount, edit_count: getEditCount(), all_read_only_streak: _allReadOnlyStreak },
94
- agent: sessionAgent || null,
95
- });
96
- } catch { /* best-effort */ }
97
- };
98
-
99
- return {
100
- // Post-batch steering hint gate. `hintAlreadyFired` seeds the once-per-turn
101
- // latch. Returns nothing;
102
- // pushes at most one steering message via the ctx push callbacks.
103
- emitPostBatchSteering(calls, hintAlreadyFired) {
104
- const iterations = getIterations();
105
- const editCount = getEditCount();
106
- // Steering hint gate: at most ONE hint per turn.
107
- let _hintFiredThisTurn = hintAlreadyFired;
108
- // Missed-parallelism steering: 2+ consecutive turns of a single
109
- // read-only tool call suggest the model isn't batching independent
110
- // lookups. Nudge once, then reset (fires again after 2 more).
111
- if (calls.length === 1 && isEagerDispatchable(calls[0].name, tools)) {
112
- _serialReadOnlyStreak += 1;
113
- // Escalation ladder (Step 1). Cumulative level-1 fires are tracked
114
- // and NEVER reset. Once level-1 has fired >=3 times with ZERO edits,
115
- // escalate to level-2 steering (latched once per 5 turns). The
116
- // level-2 escalation always wins.
117
- const _canEscalate = (_level1FireCount + 1) >= 3 && editCount === 0 && (iterations - _level2LatchAtIteration) >= 5;
118
- if (_serialReadOnlyStreak >= 2 && !_hintFiredThisTurn) {
119
- _serialReadOnlyStreak = 0;
120
- _level1FireCount += 1;
121
- if (_canEscalate) {
122
- _emitLevel2Steer();
123
- } else {
124
- pushSystemReminder(editPushEligible
125
- ? 'Last 2 turns each ran a single read-only tool. Batch independent lookups (read/grep/glob/code_graph) into ONE turn, or start editing if you have enough context.'
126
- : 'Last 2 turns each ran a single read-only tool. Batch independent lookups (read/grep/glob/code_graph) into ONE turn.');
127
- try {
128
- appendAgentTrace({
129
- sessionId,
130
- iteration: iterations,
131
- kind: 'steer',
132
- payload: { tag: 'level1_batching', level1_fires: _level1FireCount, edit_count: editCount },
133
- agent: sessionAgent || null,
134
- });
135
- } catch { /* best-effort */ }
136
- }
137
- _hintFiredThisTurn = true;
138
- }
139
- } else {
140
- _serialReadOnlyStreak = 0;
141
- }
142
- // Independent all-read-only escalation (audit finding): the level-1
143
- // streak above only counts single-call turns, so a worker that runs
144
- // 2+ read-only calls per turn escapes the ladder entirely. Track a
145
- // cumulative count of consecutive turns where EVERY call is read-only
146
- // (any count) and no edit has been made; at 12 such turns fire level-2
147
- // directly (same once-per-5-turn latch), reset on any edit.
148
- {
149
- const _allReadOnly = calls.length > 0 && calls.every((c) => isEagerDispatchable(c.name, tools));
150
- if (_allReadOnly && editCount === 0) {
151
- _allReadOnlyStreak += 1;
152
- if (_allReadOnlyStreak >= 12 && (iterations - _level2LatchAtIteration) >= 5 && !_hintFiredThisTurn) {
153
- _emitLevel2Steer();
154
- _hintFiredThisTurn = true;
155
- }
156
- } else {
157
- _allReadOnlyStreak = 0;
158
- }
159
- }
160
- // Detector: read-only shell (git status/log/diff --stat, ls/dir/cat/
161
- // type/Get-ChildItem) inspects state the dedicated tools cover.
162
- // Nudge toward them; never blocks execution.
163
- {
164
- if (!_hintFiredThisTurn && calls.some((c) => c?.name === 'shell' && _isReadOnlyShellCmd(c?.arguments?.command))) {
165
- pushSystemReminder('Read-only shell (git status/log/diff --stat, ls/dir/cat/type/Get-ChildItem) inspects state the dedicated tools cover — use grep/read/list/find/code_graph; shell is for changing state or running programs.');
166
- _hintFiredThisTurn = true;
167
- }
168
- }
169
- },
170
- // A zero-tool turn must not bridge any cross-turn ladder streak.
171
- resetAllReadOnlyStreak() {
172
- _allReadOnlyStreak = 0;
173
- _serialReadOnlyStreak = 0;
174
- },
175
- };
176
- }