u-foo 1.0.6 → 1.1.9

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 (149) hide show
  1. package/README.md +44 -4
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +11 -2
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +154 -0
  64. package/src/chat/index.js +935 -2909
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +132 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1580 -0
  98. package/src/config.js +47 -1
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +661 -488
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. package/src/bus/API_DESIGN.md +0 -204
@@ -0,0 +1,165 @@
1
+ function createChatLayout(options = {}) {
2
+ const {
3
+ blessed,
4
+ currentInputHeight = 4,
5
+ version = "unknown",
6
+ } = options;
7
+
8
+ const screen = blessed.screen({
9
+ smartCSR: true,
10
+ title: "ufoo chat",
11
+ fullUnicode: true,
12
+ // Toggle mouse at runtime to balance copy vs scroll
13
+ sendFocus: true,
14
+ mouse: false,
15
+ // Allow Ctrl+C to exit even when input grabs keys
16
+ ignoreLocked: ["C-c"],
17
+ });
18
+ // Prefer normal buffer for reliable terminal selection/copy
19
+ if (screen.program && typeof screen.program.normalBuffer === "function") {
20
+ screen.program.normalBuffer();
21
+ if (screen.program.put && typeof screen.program.put.keypad_local === "function") {
22
+ screen.program.put.keypad_local();
23
+ }
24
+ if (typeof screen.program.clear === "function") {
25
+ screen.program.clear();
26
+ screen.program.cup(0, 0);
27
+ }
28
+ }
29
+
30
+ // Log area (no border for cleaner look)
31
+ const logBox = blessed.log({
32
+ parent: screen,
33
+ top: 0,
34
+ left: 0,
35
+ width: "100%",
36
+ height: "100%-5", // Will be adjusted dynamically
37
+ tags: true,
38
+ scrollable: true,
39
+ alwaysScroll: true,
40
+ scrollback: 10000,
41
+ scrollbar: null,
42
+ keys: true,
43
+ vi: true,
44
+ // Mouse handled globally (toggleable) to keep copy working
45
+ mouse: false,
46
+ // Ensure proper wrapping and width calculation
47
+ wrap: true,
48
+ });
49
+
50
+ // Status line just above input
51
+ const statusLine = blessed.box({
52
+ parent: screen,
53
+ bottom: currentInputHeight,
54
+ left: 0,
55
+ width: "100%",
56
+ height: 1,
57
+ style: { fg: "gray" },
58
+ tags: true,
59
+ content: "",
60
+ });
61
+ const bannerText = `{bold}UFOO{/bold} · Multi-Agent Manager{|}v${version}`;
62
+ statusLine.setContent(bannerText);
63
+
64
+ // Command completion panel
65
+ const completionPanel = blessed.box({
66
+ parent: screen,
67
+ bottom: currentInputHeight - 1,
68
+ left: 0,
69
+ width: "100%",
70
+ height: 0,
71
+ hidden: true,
72
+ wrap: false,
73
+ border: {
74
+ type: "line",
75
+ top: true,
76
+ left: false,
77
+ right: false,
78
+ bottom: false,
79
+ },
80
+ style: {
81
+ border: { fg: "yellow" },
82
+ fg: "white",
83
+ // No bg - uses terminal default background
84
+ },
85
+ padding: {
86
+ left: 0,
87
+ right: 0,
88
+ top: 0,
89
+ bottom: 0,
90
+ },
91
+ tags: true,
92
+ });
93
+
94
+ // Dashboard at very bottom
95
+ const dashboard = blessed.box({
96
+ parent: screen,
97
+ bottom: 0,
98
+ left: 0,
99
+ width: "100%",
100
+ height: 1,
101
+ style: { fg: "gray" },
102
+ tags: true,
103
+ });
104
+
105
+ // Bottom border line for input area (above dashboard)
106
+ const inputBottomLine = blessed.line({
107
+ parent: screen,
108
+ bottom: 1,
109
+ left: 1,
110
+ width: "100%-2",
111
+ orientation: "horizontal",
112
+ style: { fg: "gray" },
113
+ });
114
+
115
+ // Prompt indicator
116
+ const promptBox = blessed.box({
117
+ parent: screen,
118
+ bottom: 2,
119
+ left: 0,
120
+ width: 2,
121
+ height: currentInputHeight - 3,
122
+ content: ">",
123
+ style: { fg: "cyan" },
124
+ });
125
+
126
+ // Input area without left/right border
127
+ const input = blessed.textarea({
128
+ parent: screen,
129
+ bottom: 2,
130
+ left: 2,
131
+ width: "100%-2",
132
+ height: currentInputHeight - 3,
133
+ inputOnFocus: true,
134
+ keys: true,
135
+ });
136
+ // Avoid textarea's extra wrap margin (causes a phantom empty column)
137
+ input.type = "box";
138
+
139
+ // Top border line for input area (just above input)
140
+ const inputTopLine = blessed.line({
141
+ parent: screen,
142
+ bottom: currentInputHeight - 1, // 4-1=3: above input(2) + inputHeight(1)
143
+ left: 1,
144
+ width: "100%-2",
145
+ orientation: "horizontal",
146
+ style: { fg: "gray" },
147
+ });
148
+
149
+ return {
150
+ screen,
151
+ logBox,
152
+ statusLine,
153
+ bannerText,
154
+ completionPanel,
155
+ dashboard,
156
+ inputBottomLine,
157
+ promptBox,
158
+ input,
159
+ inputTopLine,
160
+ };
161
+ }
162
+
163
+ module.exports = {
164
+ createChatLayout,
165
+ };
@@ -0,0 +1,81 @@
1
+ function createPasteController(options = {}) {
2
+ const {
3
+ shouldHandle = () => true,
4
+ normalizePaste = (text) => text,
5
+ insertTextAtCursor = () => {},
6
+ setImmediateFn = setImmediate,
7
+ clearImmediateFn = clearImmediate,
8
+ } = options;
9
+
10
+ const PASTE_START = "\x1b[200~";
11
+ const PASTE_END = "\x1b[201~";
12
+ let pasteActive = false;
13
+ let pasteBuffer = "";
14
+ let pasteRemainder = "";
15
+ let suppressKeypress = false;
16
+ let suppressReset = null;
17
+
18
+ function keepMarkerPrefixTail(text, marker) {
19
+ const max = Math.min(marker.length - 1, text.length);
20
+ for (let len = max; len > 0; len -= 1) {
21
+ if (text.endsWith(marker.slice(0, len))) {
22
+ return text.slice(-len);
23
+ }
24
+ }
25
+ return "";
26
+ }
27
+
28
+ function scheduleSuppressReset() {
29
+ suppressKeypress = true;
30
+ if (suppressReset) clearImmediateFn(suppressReset);
31
+ suppressReset = setImmediateFn(() => {
32
+ if (!pasteActive) suppressKeypress = false;
33
+ });
34
+ }
35
+
36
+ function handleProgramData(data) {
37
+ if (!shouldHandle()) return;
38
+ let buffer = pasteRemainder + data.toString("utf8");
39
+ pasteRemainder = "";
40
+ while (buffer.length > 0) {
41
+ if (!pasteActive) {
42
+ const start = buffer.indexOf(PASTE_START);
43
+ if (start === -1) {
44
+ pasteRemainder = keepMarkerPrefixTail(buffer, PASTE_START);
45
+ return;
46
+ }
47
+ buffer = buffer.slice(start + PASTE_START.length);
48
+ pasteActive = true;
49
+ pasteBuffer = "";
50
+ scheduleSuppressReset();
51
+ continue;
52
+ }
53
+ const end = buffer.indexOf(PASTE_END);
54
+ if (end === -1) {
55
+ pasteBuffer += buffer;
56
+ scheduleSuppressReset();
57
+ return;
58
+ }
59
+ pasteBuffer += buffer.slice(0, end);
60
+ buffer = buffer.slice(end + PASTE_END.length);
61
+ pasteActive = false;
62
+ scheduleSuppressReset();
63
+ const normalized = normalizePaste(pasteBuffer);
64
+ pasteBuffer = "";
65
+ if (normalized) insertTextAtCursor(normalized);
66
+ }
67
+ }
68
+
69
+ function isSuppressKeypress() {
70
+ return suppressKeypress;
71
+ }
72
+
73
+ return {
74
+ handleProgramData,
75
+ isSuppressKeypress,
76
+ };
77
+ }
78
+
79
+ module.exports = {
80
+ createPasteController,
81
+ };
@@ -0,0 +1,42 @@
1
+ function keyToRaw(ch, key) {
2
+ if (ch && ch.length === 1) return ch;
3
+ if (!key) return null;
4
+
5
+ switch (key.name) {
6
+ case "return":
7
+ case "enter":
8
+ return "\r";
9
+ case "backspace":
10
+ return "\x7f";
11
+ case "tab":
12
+ return "\t";
13
+ case "escape":
14
+ return "\x1b";
15
+ case "up":
16
+ return "\x1b[A";
17
+ case "down":
18
+ return "\x1b[B";
19
+ case "right":
20
+ return "\x1b[C";
21
+ case "left":
22
+ return "\x1b[D";
23
+ case "home":
24
+ return "\x1b[H";
25
+ case "end":
26
+ return "\x1b[F";
27
+ case "pageup":
28
+ return "\x1b[5~";
29
+ case "pagedown":
30
+ return "\x1b[6~";
31
+ case "delete":
32
+ return "\x1b[3~";
33
+ case "insert":
34
+ return "\x1b[2~";
35
+ default:
36
+ return ch || null;
37
+ }
38
+ }
39
+
40
+ module.exports = {
41
+ keyToRaw,
42
+ };
@@ -0,0 +1,132 @@
1
+ const path = require("path");
2
+
3
+ function createSettingsController(options = {}) {
4
+ const {
5
+ projectRoot,
6
+ saveConfig = () => {},
7
+ normalizeLaunchMode = (value) => value,
8
+ normalizeAgentProvider = (value) => value,
9
+ normalizeAssistantEngine = (value) => value,
10
+ fsModule,
11
+ getUfooPaths = () => ({ agentDir: "" }),
12
+ logMessage = () => {},
13
+ renderDashboard = () => {},
14
+ renderScreen = () => {},
15
+ restartDaemon = () => {},
16
+ getLaunchMode = () => "terminal",
17
+ setLaunchModeState = () => {},
18
+ setSelectedModeIndex = () => {},
19
+ getAgentProvider = () => "codex-cli",
20
+ setAgentProviderState = () => {},
21
+ setSelectedProviderIndex = () => {},
22
+ getAssistantEngine = () => "auto",
23
+ setAssistantEngineState = () => {},
24
+ setSelectedAssistantIndex = () => {},
25
+ assistantOptions = [],
26
+ getAutoResume = () => true,
27
+ setAutoResumeState = () => {},
28
+ setSelectedResumeIndex = () => {},
29
+ } = options;
30
+
31
+ if (!projectRoot) {
32
+ throw new Error("createSettingsController requires projectRoot");
33
+ }
34
+ if (!fsModule) {
35
+ throw new Error("createSettingsController requires fsModule");
36
+ }
37
+
38
+ function providerLabel(value) {
39
+ return value === "claude-cli" ? "claude" : "codex";
40
+ }
41
+
42
+ function assistantLabel(value) {
43
+ const normalized = normalizeAssistantEngine(value);
44
+ if (normalized === "codex") return "codex";
45
+ if (normalized === "claude") return "claude";
46
+ if (normalized === "ufoo") return "ufoo";
47
+ return "auto";
48
+ }
49
+
50
+ function clearUfooAgentIdentity() {
51
+ const agentDir = getUfooPaths(projectRoot).agentDir;
52
+ const stateFile = path.join(agentDir, "ufoo-agent.json");
53
+ const historyFile = path.join(agentDir, "ufoo-agent.history.jsonl");
54
+ try {
55
+ fsModule.rmSync(stateFile, { force: true });
56
+ } catch {
57
+ // Ignore cleanup failures.
58
+ }
59
+ try {
60
+ fsModule.rmSync(historyFile, { force: true });
61
+ } catch {
62
+ // Ignore cleanup failures.
63
+ }
64
+ }
65
+
66
+ function setLaunchMode(mode) {
67
+ const next = normalizeLaunchMode(mode);
68
+ if (next === getLaunchMode()) return false;
69
+ setLaunchModeState(next);
70
+ setSelectedModeIndex(next === "internal" ? 2 : (next === "tmux" ? 1 : 0));
71
+ saveConfig(projectRoot, { launchMode: next });
72
+ logMessage("status", `{white-fg}⚙{/white-fg} Launch mode: ${next}`);
73
+ renderDashboard();
74
+ renderScreen();
75
+ void restartDaemon();
76
+ return true;
77
+ }
78
+
79
+ function setAgentProvider(provider) {
80
+ const next = normalizeAgentProvider(provider);
81
+ if (next === getAgentProvider()) return false;
82
+ setAgentProviderState(next);
83
+ setSelectedProviderIndex(next === "claude-cli" ? 1 : 0);
84
+ saveConfig(projectRoot, { agentProvider: next });
85
+ clearUfooAgentIdentity();
86
+ logMessage("status", `{white-fg}⚙{/white-fg} ufoo-agent: ${providerLabel(next)}`);
87
+ renderDashboard();
88
+ renderScreen();
89
+ void restartDaemon();
90
+ return true;
91
+ }
92
+
93
+ function setAutoResume(value) {
94
+ const next = value !== false;
95
+ if (next === getAutoResume()) return false;
96
+ setAutoResumeState(next);
97
+ setSelectedResumeIndex(next ? 0 : 1);
98
+ saveConfig(projectRoot, { autoResume: next });
99
+ const label = next ? "Resume previous session" : "Start new session";
100
+ logMessage("status", `{white-fg}⚙{/white-fg} Resume mode: ${label}`);
101
+ renderDashboard();
102
+ renderScreen();
103
+ return true;
104
+ }
105
+
106
+ function setAssistantEngine(engine) {
107
+ const next = normalizeAssistantEngine(engine);
108
+ if (next === getAssistantEngine()) return false;
109
+ setAssistantEngineState(next);
110
+ const idx = assistantOptions.findIndex((opt) => opt && opt.value === next);
111
+ setSelectedAssistantIndex(idx >= 0 ? idx : 0);
112
+ saveConfig(projectRoot, { assistantEngine: next });
113
+ logMessage("status", `{white-fg}⚙{/white-fg} assistant-engine: ${assistantLabel(next)}`);
114
+ renderDashboard();
115
+ renderScreen();
116
+ return true;
117
+ }
118
+
119
+ return {
120
+ providerLabel,
121
+ assistantLabel,
122
+ clearUfooAgentIdentity,
123
+ setLaunchMode,
124
+ setAgentProvider,
125
+ setAssistantEngine,
126
+ setAutoResume,
127
+ };
128
+ }
129
+
130
+ module.exports = {
131
+ createSettingsController,
132
+ };
@@ -0,0 +1,177 @@
1
+ function createStatusLineController(options = {}) {
2
+ const {
3
+ statusLine,
4
+ bannerText = "",
5
+ renderScreen = () => {},
6
+ setIntervalFn = setInterval,
7
+ clearIntervalFn = clearInterval,
8
+ now = () => Date.now(),
9
+ } = options;
10
+
11
+ if (!statusLine) {
12
+ throw new Error("createStatusLineController requires statusLine");
13
+ }
14
+
15
+ const pendingStatusLines = [];
16
+ const busStatusQueue = [];
17
+ let primaryStatusText = bannerText;
18
+ let primaryStatusPending = false;
19
+
20
+ const shimmerStart = now();
21
+ let animationTimer = null;
22
+ const STATUS_ANIM_FRAME_MS = 50;
23
+ const SHIMMER_PADDING = 10;
24
+ const SHIMMER_BAND_HALF_WIDTH = 5;
25
+ const SHIMMER_SWEEP_MS = 2000;
26
+ const SPINNER_PERIOD_MS = 600;
27
+
28
+ function formatProcessingText(text) {
29
+ if (!text) return text;
30
+ if (text.includes("{")) return text;
31
+ if (!/processing/i.test(text)) return text;
32
+ return text;
33
+ }
34
+
35
+ function shimmerText(text, nowMs) {
36
+ if (!text) return "";
37
+ if (text.includes("{")) return text;
38
+ const chars = Array.from(text);
39
+ const period = chars.length + SHIMMER_PADDING * 2;
40
+ const pos = Math.floor(((nowMs - shimmerStart) % SHIMMER_SWEEP_MS) / SHIMMER_SWEEP_MS * period);
41
+ let out = "";
42
+ for (let i = 0; i < chars.length; i += 1) {
43
+ const iPos = i + SHIMMER_PADDING;
44
+ const dist = Math.abs(iPos - pos);
45
+ let intensity = 0;
46
+ if (dist <= SHIMMER_BAND_HALF_WIDTH) {
47
+ const x = Math.PI * (dist / SHIMMER_BAND_HALF_WIDTH);
48
+ intensity = 0.5 * (1 + Math.cos(x));
49
+ }
50
+ const ch = chars[i];
51
+ if (intensity < 0.2) {
52
+ out += `{gray-fg}${ch}{/gray-fg}`;
53
+ } else if (intensity < 0.6) {
54
+ out += ch;
55
+ } else {
56
+ out += `{bold}{white-fg}${ch}{/white-fg}{/bold}`;
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ function spinnerFrame(nowMs) {
63
+ const on = Math.floor((nowMs - shimmerStart) / SPINNER_PERIOD_MS) % 2 === 0;
64
+ return on ? "{gray-fg}•{/gray-fg}" : "{gray-fg}◦{/gray-fg}";
65
+ }
66
+
67
+ function renderPendingStatus(text, nowMs) {
68
+ const spinner = spinnerFrame(nowMs);
69
+ const shimmer = shimmerText(text, nowMs);
70
+ return shimmer ? `${spinner} ${shimmer}` : spinner;
71
+ }
72
+
73
+ function renderStatusLine(nowMs = now()) {
74
+ let content = primaryStatusText || "";
75
+ if (primaryStatusPending) {
76
+ content = renderPendingStatus(primaryStatusText, nowMs);
77
+ }
78
+ if (busStatusQueue.length > 0) {
79
+ const extra = busStatusQueue.length > 1
80
+ ? ` {gray-fg}(+${busStatusQueue.length - 1}){/gray-fg}`
81
+ : "";
82
+ const busText = `${busStatusQueue[0].text}${extra}`;
83
+ content = content
84
+ ? `${content} {gray-fg}·{/gray-fg} ${busText}`
85
+ : busText;
86
+ }
87
+ statusLine.setContent(content);
88
+ }
89
+
90
+ function updateAnimation() {
91
+ if (primaryStatusPending && !animationTimer) {
92
+ animationTimer = setIntervalFn(() => {
93
+ if (!primaryStatusPending) return;
94
+ renderStatusLine(now());
95
+ renderScreen();
96
+ }, STATUS_ANIM_FRAME_MS);
97
+ } else if (!primaryStatusPending && animationTimer) {
98
+ clearIntervalFn(animationTimer);
99
+ animationTimer = null;
100
+ }
101
+ }
102
+
103
+ function setPrimaryStatus(text, options = {}) {
104
+ primaryStatusText = text || "";
105
+ primaryStatusPending = Boolean(options.pending);
106
+ updateAnimation();
107
+ renderStatusLine();
108
+ }
109
+
110
+ function queueStatusLine(text) {
111
+ pendingStatusLines.push(text || "");
112
+ if (pendingStatusLines.length === 1) {
113
+ setPrimaryStatus(pendingStatusLines[0], { pending: true });
114
+ renderScreen();
115
+ }
116
+ }
117
+
118
+ function resolveStatusLine(text) {
119
+ if (pendingStatusLines.length > 0) {
120
+ pendingStatusLines.shift();
121
+ }
122
+ if (pendingStatusLines.length > 0) {
123
+ setPrimaryStatus(pendingStatusLines[0], { pending: true });
124
+ } else {
125
+ setPrimaryStatus(text || "", { pending: false });
126
+ }
127
+ renderScreen();
128
+ }
129
+
130
+ function enqueueBusStatus(item) {
131
+ if (!item || !item.text) return;
132
+ const key = item.key || item.text;
133
+ const formatted = formatProcessingText(item.text);
134
+ const existing = busStatusQueue.find((entry) => entry.key === key);
135
+ if (existing) {
136
+ existing.text = formatted;
137
+ } else {
138
+ busStatusQueue.push({ key, text: formatted });
139
+ }
140
+ renderStatusLine();
141
+ }
142
+
143
+ function resolveBusStatus(item) {
144
+ if (!item) return;
145
+ const key = item.key || item.text;
146
+ let index = -1;
147
+ if (key) {
148
+ index = busStatusQueue.findIndex((entry) => entry.key === key);
149
+ }
150
+ if (index === -1 && item.text) {
151
+ index = busStatusQueue.findIndex((entry) => entry.text === item.text);
152
+ }
153
+ if (index === -1) return;
154
+ busStatusQueue.splice(index, 1);
155
+ renderStatusLine();
156
+ }
157
+
158
+ function destroy() {
159
+ if (animationTimer) {
160
+ clearIntervalFn(animationTimer);
161
+ animationTimer = null;
162
+ }
163
+ }
164
+
165
+ return {
166
+ queueStatusLine,
167
+ resolveStatusLine,
168
+ enqueueBusStatus,
169
+ resolveBusStatus,
170
+ renderStatusLine,
171
+ destroy,
172
+ };
173
+ }
174
+
175
+ module.exports = {
176
+ createStatusLineController,
177
+ };