opencode-forge 0.1.5

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.

Potentially problematic release.


This version of opencode-forge might be problematic. Click here for more details.

Files changed (309) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +534 -0
  3. package/config.jsonc +47 -0
  4. package/dist/agents/architect.d.ts +3 -0
  5. package/dist/agents/architect.d.ts.map +1 -0
  6. package/dist/agents/architect.js +152 -0
  7. package/dist/agents/architect.js.map +1 -0
  8. package/dist/agents/auditor.d.ts +3 -0
  9. package/dist/agents/auditor.d.ts.map +1 -0
  10. package/dist/agents/auditor.js +168 -0
  11. package/dist/agents/auditor.js.map +1 -0
  12. package/dist/agents/code.d.ts +3 -0
  13. package/dist/agents/code.d.ts.map +1 -0
  14. package/dist/agents/code.js +67 -0
  15. package/dist/agents/code.js.map +1 -0
  16. package/dist/agents/index.d.ts +4 -0
  17. package/dist/agents/index.d.ts.map +1 -0
  18. package/dist/agents/index.js +9 -0
  19. package/dist/agents/index.js.map +1 -0
  20. package/dist/agents/prompts.d.ts +1 -0
  21. package/dist/agents/prompts.d.ts.map +1 -0
  22. package/dist/agents/prompts.js +4 -0
  23. package/dist/agents/prompts.js.map +1 -0
  24. package/dist/agents/types.d.ts +34 -0
  25. package/dist/agents/types.d.ts.map +1 -0
  26. package/dist/agents/types.js +2 -0
  27. package/dist/agents/types.js.map +1 -0
  28. package/dist/cache/index.d.ts +4 -0
  29. package/dist/cache/index.d.ts.map +1 -0
  30. package/dist/cache/index.js +5 -0
  31. package/dist/cache/index.js.map +1 -0
  32. package/dist/cache/memory-cache.d.ts +14 -0
  33. package/dist/cache/memory-cache.d.ts.map +1 -0
  34. package/dist/cache/memory-cache.js +51 -0
  35. package/dist/cache/memory-cache.js.map +1 -0
  36. package/dist/cache/types.d.ts +8 -0
  37. package/dist/cache/types.d.ts.map +1 -0
  38. package/dist/cache/types.js +2 -0
  39. package/dist/cache/types.js.map +1 -0
  40. package/dist/cli/commands/cancel.d.ts +15 -0
  41. package/dist/cli/commands/cancel.d.ts.map +1 -0
  42. package/dist/cli/commands/cancel.js +194 -0
  43. package/dist/cli/commands/cancel.js.map +1 -0
  44. package/dist/cli/commands/graph.d.ts +16 -0
  45. package/dist/cli/commands/graph.d.ts.map +1 -0
  46. package/dist/cli/commands/graph.js +208 -0
  47. package/dist/cli/commands/graph.js.map +1 -0
  48. package/dist/cli/commands/restart.d.ts +15 -0
  49. package/dist/cli/commands/restart.d.ts.map +1 -0
  50. package/dist/cli/commands/restart.js +268 -0
  51. package/dist/cli/commands/restart.js.map +1 -0
  52. package/dist/cli/commands/status.d.ts +17 -0
  53. package/dist/cli/commands/status.d.ts.map +1 -0
  54. package/dist/cli/commands/status.js +356 -0
  55. package/dist/cli/commands/status.js.map +1 -0
  56. package/dist/cli/commands/upgrade.d.ts +3 -0
  57. package/dist/cli/commands/upgrade.d.ts.map +1 -0
  58. package/dist/cli/commands/upgrade.js +40 -0
  59. package/dist/cli/commands/upgrade.js.map +1 -0
  60. package/dist/cli/index.d.ts +3 -0
  61. package/dist/cli/index.d.ts.map +1 -0
  62. package/dist/cli/index.js +224 -0
  63. package/dist/cli/index.js.map +1 -0
  64. package/dist/cli/utils.d.ts +36 -0
  65. package/dist/cli/utils.d.ts.map +1 -0
  66. package/dist/cli/utils.js +163 -0
  67. package/dist/cli/utils.js.map +1 -0
  68. package/dist/command/template/review.txt +101 -0
  69. package/dist/config.d.ts +5 -0
  70. package/dist/config.d.ts.map +1 -0
  71. package/dist/config.js +186 -0
  72. package/dist/config.js.map +1 -0
  73. package/dist/constants/loop.d.ts +10 -0
  74. package/dist/constants/loop.d.ts.map +1 -0
  75. package/dist/constants/loop.js +6 -0
  76. package/dist/constants/loop.js.map +1 -0
  77. package/dist/graph/cache.d.ts +17 -0
  78. package/dist/graph/cache.d.ts.map +1 -0
  79. package/dist/graph/cache.js +50 -0
  80. package/dist/graph/cache.js.map +1 -0
  81. package/dist/graph/client.d.ts +51 -0
  82. package/dist/graph/client.d.ts.map +1 -0
  83. package/dist/graph/client.js +152 -0
  84. package/dist/graph/client.js.map +1 -0
  85. package/dist/graph/clone-detection.d.ts +9 -0
  86. package/dist/graph/clone-detection.d.ts.map +1 -0
  87. package/dist/graph/clone-detection.js +148 -0
  88. package/dist/graph/clone-detection.js.map +1 -0
  89. package/dist/graph/constants.d.ts +18 -0
  90. package/dist/graph/constants.d.ts.map +1 -0
  91. package/dist/graph/constants.js +532 -0
  92. package/dist/graph/constants.js.map +1 -0
  93. package/dist/graph/database.d.ts +11 -0
  94. package/dist/graph/database.d.ts.map +1 -0
  95. package/dist/graph/database.js +250 -0
  96. package/dist/graph/database.js.map +1 -0
  97. package/dist/graph/index.d.ts +14 -0
  98. package/dist/graph/index.d.ts.map +1 -0
  99. package/dist/graph/index.js +13 -0
  100. package/dist/graph/index.js.map +1 -0
  101. package/dist/graph/repo-map.d.ts +59 -0
  102. package/dist/graph/repo-map.d.ts.map +1 -0
  103. package/dist/graph/repo-map.js +948 -0
  104. package/dist/graph/repo-map.js.map +1 -0
  105. package/dist/graph/rpc.d.ts +34 -0
  106. package/dist/graph/rpc.d.ts.map +1 -0
  107. package/dist/graph/rpc.js +139 -0
  108. package/dist/graph/rpc.js.map +1 -0
  109. package/dist/graph/service.d.ts +46 -0
  110. package/dist/graph/service.d.ts.map +1 -0
  111. package/dist/graph/service.js +329 -0
  112. package/dist/graph/service.js.map +1 -0
  113. package/dist/graph/tree-sitter.d.ts +40 -0
  114. package/dist/graph/tree-sitter.d.ts.map +1 -0
  115. package/dist/graph/tree-sitter.js +799 -0
  116. package/dist/graph/tree-sitter.js.map +1 -0
  117. package/dist/graph/types.d.ts +175 -0
  118. package/dist/graph/types.d.ts.map +1 -0
  119. package/dist/graph/types.js +105 -0
  120. package/dist/graph/types.js.map +1 -0
  121. package/dist/graph/utils.d.ts +64 -0
  122. package/dist/graph/utils.d.ts.map +1 -0
  123. package/dist/graph/utils.js +406 -0
  124. package/dist/graph/utils.js.map +1 -0
  125. package/dist/graph/worker.d.ts +2 -0
  126. package/dist/graph/worker.d.ts.map +1 -0
  127. package/dist/graph/worker.js +6043 -0
  128. package/dist/graph/worker.js.map +1 -0
  129. package/dist/hooks/compaction-utils.d.ts +21 -0
  130. package/dist/hooks/compaction-utils.d.ts.map +1 -0
  131. package/dist/hooks/compaction-utils.js +82 -0
  132. package/dist/hooks/compaction-utils.js.map +1 -0
  133. package/dist/hooks/graph-command.d.ts +27 -0
  134. package/dist/hooks/graph-command.d.ts.map +1 -0
  135. package/dist/hooks/graph-command.js +57 -0
  136. package/dist/hooks/graph-command.js.map +1 -0
  137. package/dist/hooks/graph-tools.d.ts +11 -0
  138. package/dist/hooks/graph-tools.d.ts.map +1 -0
  139. package/dist/hooks/graph-tools.js +125 -0
  140. package/dist/hooks/graph-tools.js.map +1 -0
  141. package/dist/hooks/index.d.ts +5 -0
  142. package/dist/hooks/index.d.ts.map +1 -0
  143. package/dist/hooks/index.js +5 -0
  144. package/dist/hooks/index.js.map +1 -0
  145. package/dist/hooks/loop.d.ts +23 -0
  146. package/dist/hooks/loop.d.ts.map +1 -0
  147. package/dist/hooks/loop.js +667 -0
  148. package/dist/hooks/loop.js.map +1 -0
  149. package/dist/hooks/sandbox-tools.d.ts +13 -0
  150. package/dist/hooks/sandbox-tools.d.ts.map +1 -0
  151. package/dist/hooks/sandbox-tools.js +105 -0
  152. package/dist/hooks/sandbox-tools.js.map +1 -0
  153. package/dist/hooks/session.d.ts +19 -0
  154. package/dist/hooks/session.d.ts.map +1 -0
  155. package/dist/hooks/session.js +56 -0
  156. package/dist/hooks/session.js.map +1 -0
  157. package/dist/index.d.ts +11 -0
  158. package/dist/index.d.ts.map +1 -0
  159. package/dist/index.js +298 -0
  160. package/dist/index.js.map +1 -0
  161. package/dist/sandbox/context.d.ts +27 -0
  162. package/dist/sandbox/context.d.ts.map +1 -0
  163. package/dist/sandbox/context.js +18 -0
  164. package/dist/sandbox/context.js.map +1 -0
  165. package/dist/sandbox/docker.d.ts +29 -0
  166. package/dist/sandbox/docker.d.ts.map +1 -0
  167. package/dist/sandbox/docker.js +213 -0
  168. package/dist/sandbox/docker.js.map +1 -0
  169. package/dist/sandbox/manager.d.ts +23 -0
  170. package/dist/sandbox/manager.d.ts.map +1 -0
  171. package/dist/sandbox/manager.js +131 -0
  172. package/dist/sandbox/manager.js.map +1 -0
  173. package/dist/sandbox/path.d.ts +4 -0
  174. package/dist/sandbox/path.d.ts.map +1 -0
  175. package/dist/sandbox/path.js +27 -0
  176. package/dist/sandbox/path.js.map +1 -0
  177. package/dist/services/kv.d.ts +17 -0
  178. package/dist/services/kv.d.ts.map +1 -0
  179. package/dist/services/kv.js +62 -0
  180. package/dist/services/kv.js.map +1 -0
  181. package/dist/services/loop.d.ts +96 -0
  182. package/dist/services/loop.d.ts.map +1 -0
  183. package/dist/services/loop.js +315 -0
  184. package/dist/services/loop.js.map +1 -0
  185. package/dist/setup.d.ts +4 -0
  186. package/dist/setup.d.ts.map +1 -0
  187. package/dist/setup.js +118 -0
  188. package/dist/setup.js.map +1 -0
  189. package/dist/storage/database.d.ts +6 -0
  190. package/dist/storage/database.d.ts.map +1 -0
  191. package/dist/storage/database.js +90 -0
  192. package/dist/storage/database.js.map +1 -0
  193. package/dist/storage/graph-projects.d.ts +80 -0
  194. package/dist/storage/graph-projects.d.ts.map +1 -0
  195. package/dist/storage/graph-projects.js +154 -0
  196. package/dist/storage/graph-projects.js.map +1 -0
  197. package/dist/storage/index.d.ts +5 -0
  198. package/dist/storage/index.d.ts.map +1 -0
  199. package/dist/storage/index.js +3 -0
  200. package/dist/storage/index.js.map +1 -0
  201. package/dist/storage/kv-queries.d.ts +18 -0
  202. package/dist/storage/kv-queries.d.ts.map +1 -0
  203. package/dist/storage/kv-queries.js +70 -0
  204. package/dist/storage/kv-queries.js.map +1 -0
  205. package/dist/tools/graph.d.ts +9 -0
  206. package/dist/tools/graph.d.ts.map +1 -0
  207. package/dist/tools/graph.js +272 -0
  208. package/dist/tools/graph.js.map +1 -0
  209. package/dist/tools/index.d.ts +6 -0
  210. package/dist/tools/index.d.ts.map +1 -0
  211. package/dist/tools/index.js +16 -0
  212. package/dist/tools/index.js.map +1 -0
  213. package/dist/tools/loop.d.ts +21 -0
  214. package/dist/tools/loop.d.ts.map +1 -0
  215. package/dist/tools/loop.js +570 -0
  216. package/dist/tools/loop.js.map +1 -0
  217. package/dist/tools/plan-approval.d.ts +15 -0
  218. package/dist/tools/plan-approval.d.ts.map +1 -0
  219. package/dist/tools/plan-approval.js +203 -0
  220. package/dist/tools/plan-approval.js.map +1 -0
  221. package/dist/tools/plan-execute.d.ts +4 -0
  222. package/dist/tools/plan-execute.d.ts.map +1 -0
  223. package/dist/tools/plan-execute.js +85 -0
  224. package/dist/tools/plan-execute.js.map +1 -0
  225. package/dist/tools/plan-kv.d.ts +4 -0
  226. package/dist/tools/plan-kv.d.ts.map +1 -0
  227. package/dist/tools/plan-kv.js +107 -0
  228. package/dist/tools/plan-kv.js.map +1 -0
  229. package/dist/tools/review.d.ts +4 -0
  230. package/dist/tools/review.d.ts.map +1 -0
  231. package/dist/tools/review.js +90 -0
  232. package/dist/tools/review.js.map +1 -0
  233. package/dist/tools/sandbox-fs.d.ts +22 -0
  234. package/dist/tools/sandbox-fs.d.ts.map +1 -0
  235. package/dist/tools/sandbox-fs.js +83 -0
  236. package/dist/tools/sandbox-fs.js.map +1 -0
  237. package/dist/tools/types.d.ts +26 -0
  238. package/dist/tools/types.d.ts.map +1 -0
  239. package/dist/tools/types.js +2 -0
  240. package/dist/tools/types.js.map +1 -0
  241. package/dist/tui.d.ts +3 -0
  242. package/dist/tui.js +2061 -0
  243. package/dist/types.d.ts +124 -0
  244. package/dist/types.d.ts.map +1 -0
  245. package/dist/types.js +2 -0
  246. package/dist/types.js.map +1 -0
  247. package/dist/utils/git-branch.d.ts +11 -0
  248. package/dist/utils/git-branch.d.ts.map +1 -0
  249. package/dist/utils/git-branch.js +35 -0
  250. package/dist/utils/git-branch.js.map +1 -0
  251. package/dist/utils/graph-status-store.d.ts +72 -0
  252. package/dist/utils/graph-status-store.d.ts.map +1 -0
  253. package/dist/utils/graph-status-store.js +62 -0
  254. package/dist/utils/graph-status-store.js.map +1 -0
  255. package/dist/utils/logger.d.ts +8 -0
  256. package/dist/utils/logger.d.ts.map +1 -0
  257. package/dist/utils/logger.js +89 -0
  258. package/dist/utils/logger.js.map +1 -0
  259. package/dist/utils/loop-format.d.ts +5 -0
  260. package/dist/utils/loop-format.d.ts.map +1 -0
  261. package/dist/utils/loop-format.js +29 -0
  262. package/dist/utils/loop-format.js.map +1 -0
  263. package/dist/utils/loop-helpers.d.ts +9 -0
  264. package/dist/utils/loop-helpers.d.ts.map +1 -0
  265. package/dist/utils/loop-helpers.js +20 -0
  266. package/dist/utils/loop-helpers.js.map +1 -0
  267. package/dist/utils/loop-launch.d.ts +32 -0
  268. package/dist/utils/loop-launch.d.ts.map +1 -0
  269. package/dist/utils/loop-launch.js +162 -0
  270. package/dist/utils/loop-launch.js.map +1 -0
  271. package/dist/utils/model-fallback.d.ts +27 -0
  272. package/dist/utils/model-fallback.d.ts.map +1 -0
  273. package/dist/utils/model-fallback.js +33 -0
  274. package/dist/utils/model-fallback.js.map +1 -0
  275. package/dist/utils/partial-match.d.ts +7 -0
  276. package/dist/utils/partial-match.d.ts.map +1 -0
  277. package/dist/utils/partial-match.js +56 -0
  278. package/dist/utils/partial-match.js.map +1 -0
  279. package/dist/utils/plan-execution.d.ts +65 -0
  280. package/dist/utils/plan-execution.d.ts.map +1 -0
  281. package/dist/utils/plan-execution.js +107 -0
  282. package/dist/utils/plan-execution.js.map +1 -0
  283. package/dist/utils/session-stats.d.ts +36 -0
  284. package/dist/utils/session-stats.d.ts.map +1 -0
  285. package/dist/utils/session-stats.js +145 -0
  286. package/dist/utils/session-stats.js.map +1 -0
  287. package/dist/utils/tui-graph-status.d.ts +38 -0
  288. package/dist/utils/tui-graph-status.d.ts.map +1 -0
  289. package/dist/utils/tui-graph-status.js +95 -0
  290. package/dist/utils/tui-graph-status.js.map +1 -0
  291. package/dist/utils/tui-plan-store.d.ts +54 -0
  292. package/dist/utils/tui-plan-store.d.ts.map +1 -0
  293. package/dist/utils/tui-plan-store.js +168 -0
  294. package/dist/utils/tui-plan-store.js.map +1 -0
  295. package/dist/utils/tui-refresh-helpers.d.ts +44 -0
  296. package/dist/utils/tui-refresh-helpers.d.ts.map +1 -0
  297. package/dist/utils/tui-refresh-helpers.js +120 -0
  298. package/dist/utils/tui-refresh-helpers.js.map +1 -0
  299. package/dist/utils/upgrade.d.ts +23 -0
  300. package/dist/utils/upgrade.d.ts.map +1 -0
  301. package/dist/utils/upgrade.js +111 -0
  302. package/dist/utils/upgrade.js.map +1 -0
  303. package/dist/version.d.ts +2 -0
  304. package/dist/version.d.ts.map +1 -0
  305. package/dist/version.js +2 -0
  306. package/dist/version.js.map +1 -0
  307. package/package.json +92 -0
  308. package/scripts/build.ts +67 -0
  309. package/src/command/template/review.txt +101 -0
package/dist/tui.js ADDED
@@ -0,0 +1,2061 @@
1
+ // src/tui.tsx
2
+ import { insert as _$insert } from "@opentui/solid";
3
+ import { use as _$use } from "@opentui/solid";
4
+ import { createComponent as _$createComponent } from "@opentui/solid";
5
+ import { effect as _$effect } from "@opentui/solid";
6
+ import { memo as _$memo } from "@opentui/solid";
7
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
8
+ import { insertNode as _$insertNode } from "@opentui/solid";
9
+ import { setProp as _$setProp } from "@opentui/solid";
10
+ import { createElement as _$createElement } from "@opentui/solid";
11
+ import { createEffect, createMemo, createSignal, onCleanup, Show, For } from "solid-js";
12
+ import { SyntaxStyle } from "@opentui/core";
13
+ import { readFileSync, existsSync as existsSync6, writeFileSync } from "fs";
14
+ import { homedir as homedir2, platform as platform2 } from "os";
15
+ import { join as join6 } from "path";
16
+ import { execSync } from "child_process";
17
+ import { Database as Database5 } from "bun:sqlite";
18
+
19
+ // src/version.ts
20
+ var VERSION = "0.1.5";
21
+
22
+ // src/storage/database.ts
23
+ import { mkdirSync, existsSync } from "fs";
24
+ import { homedir, platform } from "os";
25
+ import { join } from "path";
26
+ function resolveDataDir() {
27
+ const defaultBase = join(homedir(), platform() === "win32" ? "AppData" : ".local", "share");
28
+ const xdgDataHome = process.env["XDG_DATA_HOME"] || defaultBase;
29
+ const forgeDir = join(xdgDataHome, "opencode", "forge");
30
+ const legacyGraphDir = join(xdgDataHome, "opencode", "graph");
31
+ return existsSync(legacyGraphDir) && !existsSync(forgeDir) ? legacyGraphDir : forgeDir;
32
+ }
33
+ // src/storage/kv-queries.ts
34
+ function mapRow(row) {
35
+ return {
36
+ projectId: row.project_id,
37
+ key: row.key,
38
+ data: row.data,
39
+ expiresAt: row.expires_at,
40
+ createdAt: row.created_at,
41
+ updatedAt: row.updated_at
42
+ };
43
+ }
44
+ var CLEANUP_INTERVAL_MS = 300000;
45
+ var lastCleanupAt = 0;
46
+ function createKvQuery(db) {
47
+ const getStmt = db.prepare(`SELECT project_id, key, data, expires_at, created_at, updated_at
48
+ FROM project_kv
49
+ WHERE project_id = ? AND key = ? AND expires_at > ?`);
50
+ const setStmt = db.prepare(`INSERT INTO project_kv (project_id, key, data, expires_at, created_at, updated_at)
51
+ VALUES (?, ?, ?, ?, ?, ?)
52
+ ON CONFLICT(project_id, key) DO UPDATE SET
53
+ data = excluded.data,
54
+ expires_at = excluded.expires_at,
55
+ updated_at = excluded.updated_at`);
56
+ const deleteStmt = db.prepare(`DELETE FROM project_kv WHERE project_id = ? AND key = ?`);
57
+ const listStmt = db.prepare(`SELECT project_id, key, data, expires_at, created_at, updated_at
58
+ FROM project_kv
59
+ WHERE project_id = ? AND expires_at > ?
60
+ ORDER BY updated_at DESC`);
61
+ const listByPrefixStmt = db.prepare(`SELECT project_id, key, data, expires_at, created_at, updated_at
62
+ FROM project_kv
63
+ WHERE project_id = ? AND key LIKE ? AND expires_at > ?
64
+ ORDER BY updated_at DESC`);
65
+ const deleteExpiredStmt = db.prepare(`DELETE FROM project_kv WHERE expires_at < ?`);
66
+ return {
67
+ get(projectId, key) {
68
+ const row = getStmt.get(projectId, key, Date.now());
69
+ return row ? mapRow(row) : undefined;
70
+ },
71
+ set(projectId, key, data, expiresAt) {
72
+ const now = Date.now();
73
+ setStmt.run(projectId, key, data, expiresAt, now, now);
74
+ if (now - lastCleanupAt > CLEANUP_INTERVAL_MS) {
75
+ lastCleanupAt = now;
76
+ setImmediate(() => {
77
+ try {
78
+ deleteExpiredStmt.run(now);
79
+ } catch {}
80
+ });
81
+ }
82
+ },
83
+ delete(projectId, key) {
84
+ deleteStmt.run(projectId, key);
85
+ },
86
+ list(projectId) {
87
+ const rows = listStmt.all(projectId, Date.now());
88
+ return rows.map(mapRow);
89
+ },
90
+ listByPrefix(projectId, prefix) {
91
+ const rows = listByPrefixStmt.all(projectId, `${prefix}%`, Date.now());
92
+ return rows.map(mapRow);
93
+ },
94
+ deleteExpired() {
95
+ const result = deleteExpiredStmt.run(Date.now());
96
+ return result.changes;
97
+ }
98
+ };
99
+ }
100
+ // src/utils/session-stats.ts
101
+ function extractActivity(parts) {
102
+ const toolCalls = [];
103
+ const textLines = [];
104
+ const toolLines = [];
105
+ const subtaskLines = [];
106
+ const reasoningLines = [];
107
+ for (const p of parts) {
108
+ if (p.type === "text" && typeof p.text === "string" && p.text.trim()) {
109
+ textLines.push(p.text.trim());
110
+ } else if (p.type === "tool" && p.tool && p.state) {
111
+ const s = p.state;
112
+ const name = p.tool;
113
+ const status = s.status;
114
+ if (status === "completed") {
115
+ const title = s.title ?? name;
116
+ toolCalls.push({ tool: name, title, status: "completed" });
117
+ toolLines.push(`[done] ${name}: ${title}`);
118
+ } else if (status === "running") {
119
+ const title = s.title ?? name;
120
+ toolCalls.push({ tool: name, title, status: "running" });
121
+ toolLines.push(`[running] ${name}: ${title}`);
122
+ } else if (status === "error") {
123
+ const msg = s.error ?? "error";
124
+ toolCalls.push({ tool: name, title: msg, status: "error" });
125
+ toolLines.push(`[error] ${name}: ${msg}`);
126
+ } else if (status === "pending") {
127
+ toolCalls.push({ tool: name, title: name, status: "pending" });
128
+ toolLines.push(`[pending] ${name}`);
129
+ }
130
+ } else if (p.type === "subtask" && p.description) {
131
+ const agentLabel = p.agent ? `${p.agent}: ` : "";
132
+ subtaskLines.push(`-> ${agentLabel}${p.description}`);
133
+ } else if (p.type === "reasoning" && typeof p.text === "string" && p.text.trim()) {
134
+ reasoningLines.push(p.text.trim());
135
+ }
136
+ }
137
+ let summary = "";
138
+ if (textLines.length > 0) {
139
+ summary = textLines.join(`
140
+ `);
141
+ } else if (toolLines.length > 0) {
142
+ summary = toolLines.join(`
143
+ `);
144
+ } else if (subtaskLines.length > 0) {
145
+ summary = subtaskLines.join(`
146
+ `);
147
+ } else if (reasoningLines.length > 0) {
148
+ summary = reasoningLines.join(`
149
+ `);
150
+ }
151
+ if (!summary && toolCalls.length === 0)
152
+ return null;
153
+ return { summary, toolCalls };
154
+ }
155
+ async function fetchSessionStats(api, sessionId, directory) {
156
+ if (!directory || !sessionId) {
157
+ return null;
158
+ }
159
+ try {
160
+ const messagesResult = await api.client.session.messages({
161
+ sessionID: sessionId,
162
+ directory
163
+ });
164
+ const messages = messagesResult.data ?? [];
165
+ const assistantMessages = messages.filter((m) => m.info.role === "assistant");
166
+ let lastActivity = null;
167
+ for (let i = assistantMessages.length - 1;i >= Math.max(0, assistantMessages.length - 3); i--) {
168
+ const result = extractActivity(assistantMessages[i].parts);
169
+ if (result) {
170
+ lastActivity = result;
171
+ break;
172
+ }
173
+ }
174
+ let totalInputTokens = 0;
175
+ let totalOutputTokens = 0;
176
+ let totalReasoningTokens = 0;
177
+ let totalCacheRead = 0;
178
+ let totalCacheWrite = 0;
179
+ let totalCost = 0;
180
+ for (const msg of messages) {
181
+ totalCost += msg.info.cost ?? 0;
182
+ const tokens = msg.info.tokens;
183
+ if (tokens) {
184
+ totalInputTokens += tokens.input ?? 0;
185
+ totalOutputTokens += tokens.output ?? 0;
186
+ totalReasoningTokens += tokens.reasoning ?? 0;
187
+ totalCacheRead += tokens.cache?.read ?? 0;
188
+ totalCacheWrite += tokens.cache?.write ?? 0;
189
+ }
190
+ }
191
+ const sessionResult = await api.client.session.get({
192
+ sessionID: sessionId,
193
+ directory
194
+ });
195
+ const session = sessionResult.data;
196
+ const fileChanges = session?.summary ? {
197
+ additions: session.summary.additions,
198
+ deletions: session.summary.deletions,
199
+ files: session.summary.files
200
+ } : null;
201
+ const timing = session?.time?.created && session?.time?.updated ? {
202
+ created: session.time.created,
203
+ updated: session.time.updated,
204
+ durationMs: new Date(session.time.updated).getTime() - new Date(session.time.created).getTime()
205
+ } : null;
206
+ return {
207
+ tokens: {
208
+ input: totalInputTokens,
209
+ output: totalOutputTokens,
210
+ reasoning: totalReasoningTokens,
211
+ cacheRead: totalCacheRead,
212
+ cacheWrite: totalCacheWrite,
213
+ total: totalInputTokens + totalOutputTokens + totalReasoningTokens + totalCacheRead + totalCacheWrite
214
+ },
215
+ cost: totalCost,
216
+ messages: {
217
+ total: messages.length,
218
+ assistant: assistantMessages.length
219
+ },
220
+ fileChanges,
221
+ timing,
222
+ lastActivity
223
+ };
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+
229
+ // src/utils/logger.ts
230
+ var MAX_LOG_FILE_SIZE = 10 * 1024 * 1024;
231
+ function slugify(text) {
232
+ return text.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").replace(/-+/g, "-").substring(0, 50);
233
+ }
234
+
235
+ // src/utils/plan-execution.ts
236
+ var PLAN_EXECUTION_LABELS = [
237
+ "New session",
238
+ "Execute here",
239
+ "Loop (worktree)",
240
+ "Loop"
241
+ ];
242
+ function extractPlanTitle(planContent) {
243
+ const headingMatch = planContent.match(/^#+\s+(.+)$/m);
244
+ if (headingMatch?.[1]) {
245
+ const title = headingMatch[1].trim();
246
+ return title.length > 60 ? `${title.substring(0, 57)}...` : title;
247
+ }
248
+ const firstLine = planContent.split(`
249
+ `)[0]?.trim();
250
+ if (firstLine) {
251
+ return firstLine.length > 60 ? `${firstLine.substring(0, 57)}...` : firstLine;
252
+ }
253
+ return "Implementation Plan";
254
+ }
255
+ function extractLoopName(planContent) {
256
+ const loopNameMatch = planContent.match(/^(?:\s*(?:-\s*)?)?(?:\*\*)?Loop Name(?:\*\*)?:\s*(.+)$/m);
257
+ if (loopNameMatch?.[1]) {
258
+ const name = loopNameMatch[1].trim();
259
+ return name.length > 60 ? name.substring(0, 60) : name;
260
+ }
261
+ const title = extractPlanTitle(planContent);
262
+ return title;
263
+ }
264
+ function extractLoopNames(planContent) {
265
+ const displayName = extractLoopName(planContent);
266
+ const executionName = sanitizeLoopName(displayName);
267
+ return { displayName, executionName };
268
+ }
269
+ function sanitizeLoopName(name) {
270
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 60) || "loop";
271
+ }
272
+ function normalizeModeLabel(label) {
273
+ return label.toLowerCase();
274
+ }
275
+ function matchExecutionLabel(input) {
276
+ const normalized = normalizeModeLabel(input);
277
+ for (const label of PLAN_EXECUTION_LABELS) {
278
+ if (normalized === label.toLowerCase() || normalized.startsWith(label.toLowerCase())) {
279
+ return label;
280
+ }
281
+ }
282
+ return null;
283
+ }
284
+
285
+ // src/utils/loop-launch.ts
286
+ import { Database } from "bun:sqlite";
287
+ import { existsSync as existsSync2 } from "fs";
288
+ import { join as join2 } from "path";
289
+
290
+ // src/constants/loop.ts
291
+ var LOOP_PERMISSION_RULESET = [
292
+ { permission: "*", pattern: "*", action: "allow" },
293
+ { permission: "external_directory", pattern: "*", action: "deny" },
294
+ { permission: "bash", pattern: "git push *", action: "deny" }
295
+ ];
296
+ // src/services/loop.ts
297
+ var DEFAULT_COMPLETION_SIGNAL = "ALL_PHASES_COMPLETE";
298
+ function buildCompletionSignalInstructions(signal) {
299
+ return `
300
+
301
+ ---
302
+
303
+ **IMPORTANT - Completion Signal:** When you have completed ALL phases of this plan successfully, you MUST output the following phrase exactly: ${signal}
304
+
305
+ Before outputting the completion signal, you MUST:
306
+ 1. Verify each phase's acceptance criteria are met
307
+ 2. Run all verification commands listed in the plan and confirm they pass
308
+ 3. If tests were required, confirm they exist AND pass
309
+
310
+ Do NOT output this phrase until every phase is truly complete and all verification steps pass. The loop will continue until this signal is detected.`;
311
+ }
312
+ function generateUniqueName(baseName, existingNames) {
313
+ const maxLength = 25;
314
+ const truncated = baseName.length > maxLength ? baseName.substring(0, maxLength) : baseName;
315
+ if (!existingNames.includes(truncated)) {
316
+ return truncated;
317
+ }
318
+ let counter = 1;
319
+ let candidate = `${truncated}-${counter}`;
320
+ while (existingNames.includes(candidate)) {
321
+ counter++;
322
+ candidate = `${truncated}-${counter}`;
323
+ }
324
+ return candidate;
325
+ }
326
+
327
+ // src/utils/loop-launch.ts
328
+ async function launchFreshLoop(options) {
329
+ const { planText, title, directory, projectId, isWorktree, api } = options;
330
+ const { displayName, executionName } = extractLoopNames(planText);
331
+ const dbPath = options.dbPath ?? join2(resolveDataDir(), "graph.db");
332
+ const existingNames = [];
333
+ if (existsSync2(dbPath)) {
334
+ let db = null;
335
+ try {
336
+ db = new Database(dbPath, { readonly: true });
337
+ const stmt = db.prepare("SELECT data FROM project_kv WHERE project_id = ? AND key LIKE ? AND expires_at > ?");
338
+ const rows = stmt.all(projectId, "loop:%", Date.now());
339
+ for (const row of rows) {
340
+ try {
341
+ const state = JSON.parse(row.data);
342
+ if (state?.worktreeName) {
343
+ existingNames.push(state.worktreeName);
344
+ }
345
+ } catch {}
346
+ }
347
+ } catch {} finally {
348
+ try {
349
+ db?.close();
350
+ } catch {}
351
+ }
352
+ }
353
+ const uniqueWorktreeName = generateUniqueName(executionName, existingNames);
354
+ let sessionId;
355
+ let sessionDirectory;
356
+ let worktreeBranch;
357
+ if (isWorktree) {
358
+ const worktreeResult = await api.client.worktree.create({
359
+ worktreeCreateInput: { name: uniqueWorktreeName }
360
+ });
361
+ if (worktreeResult.error || !worktreeResult.data) {
362
+ return null;
363
+ }
364
+ sessionDirectory = worktreeResult.data.directory;
365
+ worktreeBranch = worktreeResult.data.branch;
366
+ const createResult = await api.client.session.create({
367
+ title: `Loop: ${title}`,
368
+ directory: sessionDirectory
369
+ });
370
+ if (createResult.error || !createResult.data) {
371
+ return null;
372
+ }
373
+ sessionId = createResult.data.id;
374
+ } else {
375
+ const createResult = await api.client.session.create({
376
+ title: `Loop: ${title}`,
377
+ directory
378
+ });
379
+ if (createResult.error || !createResult.data) {
380
+ return null;
381
+ }
382
+ sessionId = createResult.data.id;
383
+ sessionDirectory = directory;
384
+ }
385
+ const dbExists = existsSync2(dbPath);
386
+ if (dbExists) {
387
+ let db = null;
388
+ try {
389
+ db = new Database(dbPath);
390
+ const queries = createKvQuery(db);
391
+ const now = Date.now();
392
+ const TTL_MS = 7 * 24 * 60 * 60 * 1000;
393
+ queries.set(projectId, `plan:${uniqueWorktreeName}`, JSON.stringify(planText), now + TTL_MS);
394
+ const loopState = {
395
+ active: true,
396
+ sessionId,
397
+ worktreeName: uniqueWorktreeName,
398
+ worktreeDir: sessionDirectory,
399
+ worktreeBranch,
400
+ iteration: 1,
401
+ maxIterations: 0,
402
+ completionSignal: DEFAULT_COMPLETION_SIGNAL,
403
+ startedAt: new Date().toISOString(),
404
+ prompt: planText,
405
+ phase: "coding",
406
+ audit: true,
407
+ errorCount: 0,
408
+ auditCount: 0,
409
+ worktree: isWorktree
410
+ };
411
+ queries.set(projectId, `loop:${uniqueWorktreeName}`, JSON.stringify(loopState), now + TTL_MS);
412
+ queries.set(projectId, `loop-session:${sessionId}`, JSON.stringify(uniqueWorktreeName), now + TTL_MS);
413
+ } catch {} finally {
414
+ try {
415
+ db?.close();
416
+ } catch {}
417
+ }
418
+ }
419
+ let promptText = planText;
420
+ if (DEFAULT_COMPLETION_SIGNAL) {
421
+ promptText += buildCompletionSignalInstructions(DEFAULT_COMPLETION_SIGNAL);
422
+ }
423
+ try {
424
+ await api.client.session.promptAsync({
425
+ sessionID: sessionId,
426
+ directory: sessionDirectory,
427
+ parts: [{ type: "text", text: promptText }],
428
+ agent: "code"
429
+ });
430
+ } catch {
431
+ return null;
432
+ }
433
+ return {
434
+ sessionId,
435
+ loopName: displayName,
436
+ worktreeName: uniqueWorktreeName,
437
+ isWorktree,
438
+ worktreeDir: sessionDirectory,
439
+ worktreeBranch
440
+ };
441
+ }
442
+
443
+ // src/utils/tui-plan-store.ts
444
+ import { Database as Database2 } from "bun:sqlite";
445
+ import { existsSync as existsSync3 } from "fs";
446
+ import { join as join3 } from "path";
447
+ function getDbPath() {
448
+ return join3(resolveDataDir(), "graph.db");
449
+ }
450
+ function resolvePlanKey(projectId, sessionID, dbPathOverride) {
451
+ const dbPath = dbPathOverride || getDbPath();
452
+ if (!existsSync3(dbPath)) {
453
+ return `plan:${sessionID}`;
454
+ }
455
+ let db = null;
456
+ try {
457
+ db = new Database2(dbPath, { readonly: true });
458
+ const now = Date.now();
459
+ const mappingRow = db.prepare("SELECT data FROM project_kv WHERE project_id = ? AND key = ? AND expires_at > ?").get(projectId, `loop-session:${sessionID}`, now);
460
+ if (mappingRow) {
461
+ try {
462
+ const worktreeName = JSON.parse(mappingRow.data);
463
+ if (typeof worktreeName === "string" && worktreeName) {
464
+ return `plan:${worktreeName}`;
465
+ }
466
+ } catch {}
467
+ }
468
+ } catch {} finally {
469
+ try {
470
+ db?.close();
471
+ } catch {}
472
+ }
473
+ return `plan:${sessionID}`;
474
+ }
475
+ function readPlan(projectId, sessionID, dbPathOverride) {
476
+ const dbPath = dbPathOverride || getDbPath();
477
+ if (!existsSync3(dbPath))
478
+ return null;
479
+ let db = null;
480
+ try {
481
+ db = new Database2(dbPath, { readonly: true });
482
+ const now = Date.now();
483
+ const planKey = resolvePlanKey(projectId, sessionID, dbPath);
484
+ const row = db.prepare("SELECT data FROM project_kv WHERE project_id = ? AND key = ? AND expires_at > ?").get(projectId, planKey, now);
485
+ if (!row)
486
+ return null;
487
+ const data = row.data;
488
+ if (typeof data === "string" && data.startsWith('"')) {
489
+ try {
490
+ return JSON.parse(data);
491
+ } catch {
492
+ return data;
493
+ }
494
+ }
495
+ return data;
496
+ } catch {
497
+ return null;
498
+ } finally {
499
+ try {
500
+ db?.close();
501
+ } catch {}
502
+ }
503
+ }
504
+ function writePlan(projectId, sessionID, content, dbPathOverride) {
505
+ const dbPath = dbPathOverride || getDbPath();
506
+ if (!existsSync3(dbPath))
507
+ return false;
508
+ let db = null;
509
+ try {
510
+ db = new Database2(dbPath);
511
+ const now = Date.now();
512
+ const ttl = 7 * 24 * 60 * 60 * 1000;
513
+ const planKey = resolvePlanKey(projectId, sessionID, dbPath);
514
+ db.prepare("INSERT OR REPLACE INTO project_kv (project_id, key, data, expires_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)").run(projectId, planKey, JSON.stringify(content), now + ttl, now, now);
515
+ return true;
516
+ } catch {
517
+ return false;
518
+ } finally {
519
+ try {
520
+ db?.close();
521
+ } catch {}
522
+ }
523
+ }
524
+ function deletePlan(projectId, sessionID, dbPathOverride) {
525
+ const dbPath = dbPathOverride || getDbPath();
526
+ if (!existsSync3(dbPath))
527
+ return false;
528
+ let db = null;
529
+ try {
530
+ db = new Database2(dbPath);
531
+ const planKey = resolvePlanKey(projectId, sessionID, dbPath);
532
+ const result = db.prepare("DELETE FROM project_kv WHERE project_id = ? AND key = ?").run(projectId, planKey);
533
+ return (result.changes || 0) > 0;
534
+ } catch {
535
+ return false;
536
+ } finally {
537
+ try {
538
+ db?.close();
539
+ } catch {}
540
+ }
541
+ }
542
+
543
+ // src/utils/tui-graph-status.ts
544
+ import { Database as Database3 } from "bun:sqlite";
545
+ import { existsSync as existsSync4 } from "fs";
546
+ import { join as join4 } from "path";
547
+ function getDbPath2() {
548
+ return join4(resolveDataDir(), "graph.db");
549
+ }
550
+ function readGraphStatus(projectId, dbPathOverride) {
551
+ const dbPath = dbPathOverride || getDbPath2();
552
+ if (!existsSync4(dbPath))
553
+ return null;
554
+ let db = null;
555
+ try {
556
+ db = new Database3(dbPath, { readonly: true });
557
+ const now = Date.now();
558
+ const row = db.prepare("SELECT data FROM project_kv WHERE project_id = ? AND key = ? AND expires_at > ?").get(projectId, "graph:status", now);
559
+ if (!row)
560
+ return null;
561
+ try {
562
+ return JSON.parse(row.data);
563
+ } catch {
564
+ return null;
565
+ }
566
+ } catch {
567
+ return null;
568
+ } finally {
569
+ try {
570
+ db?.close();
571
+ } catch {}
572
+ }
573
+ }
574
+ function formatGraphStatus(status) {
575
+ if (!status) {
576
+ return { text: "unavailable", color: "textMuted" };
577
+ }
578
+ switch (status.state) {
579
+ case "ready":
580
+ if (status.stats) {
581
+ return {
582
+ text: `ready · ${status.stats.files} files`,
583
+ color: "success"
584
+ };
585
+ }
586
+ return {
587
+ text: "ready",
588
+ color: "success"
589
+ };
590
+ case "indexing":
591
+ return { text: "indexing", color: "warning" };
592
+ case "initializing":
593
+ return { text: "initializing", color: "info" };
594
+ case "error":
595
+ return { text: "error", color: "error" };
596
+ case "unavailable":
597
+ default:
598
+ return { text: "unavailable", color: "textMuted" };
599
+ }
600
+ }
601
+
602
+ // src/utils/tui-refresh-helpers.ts
603
+ import { Database as Database4 } from "bun:sqlite";
604
+ import { existsSync as existsSync5 } from "fs";
605
+ import { join as join5 } from "path";
606
+ function getDbPath3() {
607
+ return join5(resolveDataDir(), "graph.db");
608
+ }
609
+ function readLoopStates(projectId, dbPathOverride) {
610
+ const dbPath = dbPathOverride || getDbPath3();
611
+ if (!existsSync5(dbPath))
612
+ return [];
613
+ let db = null;
614
+ try {
615
+ db = new Database4(dbPath, { readonly: true });
616
+ const now = Date.now();
617
+ const stmt = db.prepare("SELECT key, data FROM project_kv WHERE project_id = ? AND key LIKE ? AND expires_at > ?");
618
+ const rows = stmt.all(projectId, "loop:%", now);
619
+ const loops = [];
620
+ for (const row of rows) {
621
+ try {
622
+ const state = JSON.parse(row.data);
623
+ if (!state.worktreeName || !state.sessionId)
624
+ continue;
625
+ loops.push({
626
+ name: state.worktreeName,
627
+ phase: state.phase ?? "coding",
628
+ iteration: state.iteration ?? 0,
629
+ maxIterations: state.maxIterations ?? 0,
630
+ sessionId: state.sessionId,
631
+ active: state.active ?? false,
632
+ startedAt: state.startedAt,
633
+ completedAt: state.completedAt,
634
+ terminationReason: state.terminationReason,
635
+ worktreeBranch: state.worktreeBranch,
636
+ worktree: state.worktree ?? false,
637
+ worktreeDir: state.worktreeDir
638
+ });
639
+ } catch {}
640
+ }
641
+ return loops;
642
+ } catch {
643
+ return [];
644
+ } finally {
645
+ try {
646
+ db?.close();
647
+ } catch {}
648
+ }
649
+ }
650
+ function readLoopByName(projectId, loopName, dbPathOverride) {
651
+ const dbPath = dbPathOverride || getDbPath3();
652
+ if (!existsSync5(dbPath))
653
+ return null;
654
+ let db = null;
655
+ try {
656
+ db = new Database4(dbPath, { readonly: true });
657
+ const now = Date.now();
658
+ const key = `loop:${loopName}`;
659
+ const row = db.prepare("SELECT data FROM project_kv WHERE project_id = ? AND key = ? AND expires_at > ?").get(projectId, key, now);
660
+ if (!row)
661
+ return null;
662
+ const state = JSON.parse(row.data);
663
+ if (!state.worktreeName || !state.sessionId)
664
+ return null;
665
+ return {
666
+ name: state.worktreeName,
667
+ phase: state.phase ?? "coding",
668
+ iteration: state.iteration ?? 0,
669
+ maxIterations: state.maxIterations ?? 0,
670
+ sessionId: state.sessionId,
671
+ active: state.active ?? false,
672
+ startedAt: state.startedAt,
673
+ completedAt: state.completedAt,
674
+ terminationReason: state.terminationReason,
675
+ worktreeBranch: state.worktreeBranch,
676
+ worktree: state.worktree ?? false,
677
+ worktreeDir: state.worktreeDir
678
+ };
679
+ } catch {
680
+ return null;
681
+ } finally {
682
+ try {
683
+ db?.close();
684
+ } catch {}
685
+ }
686
+ }
687
+
688
+ // src/tui.tsx
689
+ function loadTuiConfig() {
690
+ try {
691
+ const defaultBase = join6(homedir2(), platform2() === "win32" ? "AppData" : ".config");
692
+ const configDir = process.env["XDG_CONFIG_HOME"] || defaultBase;
693
+ const configRoot = join6(configDir, "opencode");
694
+ const configPath = existsSync6(join6(configRoot, "forge-config.jsonc")) ? join6(configRoot, "forge-config.jsonc") : existsSync6(join6(configRoot, "memory-config.jsonc")) ? join6(configRoot, "memory-config.jsonc") : join6(configRoot, "graph-config.jsonc");
695
+ const raw = readFileSync(configPath, "utf-8");
696
+ const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
697
+ const parsed = JSON.parse(stripped);
698
+ return parsed?.tui;
699
+ } catch {
700
+ return;
701
+ }
702
+ }
703
+ function resolveProjectId(directory) {
704
+ const cachePath = join6(directory, ".git", "opencode");
705
+ if (existsSync6(cachePath)) {
706
+ try {
707
+ const id = readFileSync(cachePath, "utf-8").trim();
708
+ if (id)
709
+ return id;
710
+ } catch {}
711
+ }
712
+ try {
713
+ const output = execSync("git rev-list --max-parents=0 --all", {
714
+ cwd: directory,
715
+ encoding: "utf-8"
716
+ }).trim();
717
+ const commits = output.split(`
718
+ `).filter(Boolean).sort();
719
+ if (commits[0])
720
+ return commits[0];
721
+ } catch {}
722
+ return null;
723
+ }
724
+ function cancelLoop(projectId, loopName) {
725
+ const dbPath = join6(resolveDataDir(), "graph.db");
726
+ if (!existsSync6(dbPath))
727
+ return null;
728
+ let db = null;
729
+ try {
730
+ db = new Database5(dbPath);
731
+ const key = `loop:${loopName}`;
732
+ const now = Date.now();
733
+ const row = db.prepare("SELECT data, project_id FROM project_kv WHERE project_id = ? AND key = ? AND expires_at > ?").get(projectId, key, now);
734
+ if (!row)
735
+ return null;
736
+ const state = JSON.parse(row.data);
737
+ if (!state.active)
738
+ return null;
739
+ const updatedState = {
740
+ ...state,
741
+ active: false,
742
+ completedAt: new Date().toISOString(),
743
+ terminationReason: "cancelled"
744
+ };
745
+ db.prepare("UPDATE project_kv SET data = ?, updated_at = ? WHERE project_id = ? AND key = ?").run(JSON.stringify(updatedState), now, projectId, key);
746
+ return state.sessionId ?? null;
747
+ } catch {
748
+ return null;
749
+ } finally {
750
+ try {
751
+ db?.close();
752
+ } catch {}
753
+ }
754
+ }
755
+ async function restartLoop(projectId, loopName, api) {
756
+ const dbPath = join6(resolveDataDir(), "graph.db");
757
+ if (!existsSync6(dbPath))
758
+ return null;
759
+ let db = null;
760
+ try {
761
+ db = new Database5(dbPath);
762
+ const key = `loop:${loopName}`;
763
+ const now = Date.now();
764
+ const row = db.prepare("SELECT data, project_id FROM project_kv WHERE project_id = ? AND key = ? AND expires_at > ?").get(projectId, key, now);
765
+ if (!row)
766
+ return null;
767
+ const state = JSON.parse(row.data);
768
+ if (state.active) {
769
+ try {
770
+ await api.client.session.abort({
771
+ sessionID: state.sessionId
772
+ });
773
+ } catch {}
774
+ const oldSessionKey = `loop-session:${state.sessionId}`;
775
+ db.prepare("DELETE FROM project_kv WHERE project_id = ? AND key = ?").run(projectId, oldSessionKey);
776
+ }
777
+ const directory = state.worktreeDir;
778
+ if (!directory)
779
+ return null;
780
+ const createResult = await api.client.session.create({
781
+ directory,
782
+ title: loopName,
783
+ permission: LOOP_PERMISSION_RULESET
784
+ });
785
+ if (createResult.error || !createResult.data)
786
+ return null;
787
+ const newSessionId = createResult.data.id;
788
+ const sessionKey = `loop-session:${newSessionId}`;
789
+ const ttl = 30 * 24 * 60 * 60 * 1000;
790
+ db.prepare("INSERT OR REPLACE INTO project_kv (project_id, key, data, expires_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(projectId, sessionKey, JSON.stringify(loopName), now + ttl, now);
791
+ const newState = {
792
+ ...state,
793
+ active: true,
794
+ sessionId: newSessionId,
795
+ phase: "coding",
796
+ errorCount: 0,
797
+ auditCount: 0,
798
+ startedAt: new Date().toISOString(),
799
+ completedAt: undefined,
800
+ terminationReason: undefined
801
+ };
802
+ db.prepare("UPDATE project_kv SET data = ?, updated_at = ? WHERE project_id = ? AND key = ?").run(JSON.stringify(newState), now, projectId, key);
803
+ let promptText = state.prompt ?? "";
804
+ if (state.completionSignal) {
805
+ const completionInstructions = `
806
+
807
+ ---
808
+
809
+ **IMPORTANT - Completion Signal:** When you have completed ALL phases of this plan successfully, you MUST output the following phrase exactly: ${state.completionSignal}
810
+
811
+ Before outputting the completion signal, you MUST:
812
+ 1. Verify each phase's acceptance criteria are met
813
+ 2. Run all verification commands listed in the plan and confirm they pass
814
+ 3. If tests were required, confirm they exist AND pass
815
+
816
+ Do NOT output this phrase until every phase is truly complete and all verification steps pass. The loop will continue until this signal is detected.`;
817
+ promptText += completionInstructions;
818
+ }
819
+ await api.client.session.promptAsync({
820
+ sessionID: newSessionId,
821
+ directory,
822
+ parts: [{
823
+ type: "text",
824
+ text: promptText
825
+ }],
826
+ agent: "code"
827
+ });
828
+ return newSessionId;
829
+ } catch {
830
+ return null;
831
+ } finally {
832
+ try {
833
+ db?.close();
834
+ } catch {}
835
+ }
836
+ }
837
+ function formatTokens(n) {
838
+ return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`;
839
+ }
840
+ function formatDuration(ms) {
841
+ const hours = Math.floor(ms / (1000 * 60 * 60));
842
+ const minutes = Math.floor(ms % (1000 * 60 * 60) / (1000 * 60));
843
+ const seconds = Math.floor(ms % (1000 * 60) / 1000);
844
+ if (hours > 0) {
845
+ return `${hours}h ${minutes}m ${seconds}s`;
846
+ }
847
+ if (minutes > 0) {
848
+ return `${minutes}m ${seconds}s`;
849
+ }
850
+ return `${seconds}s`;
851
+ }
852
+ function truncate(text, maxLength) {
853
+ if (text.length <= maxLength)
854
+ return text;
855
+ return text.slice(0, maxLength - 3) + "...";
856
+ }
857
+ function truncateMiddle(text, maxLength) {
858
+ if (text.length <= maxLength)
859
+ return text;
860
+ const keep = maxLength - 5;
861
+ const start = Math.ceil(keep / 2);
862
+ const end = Math.floor(keep / 2);
863
+ return text.slice(0, start) + "....." + text.slice(text.length - end);
864
+ }
865
+ function PlanViewerDialog(props) {
866
+ const theme = () => props.api.theme.current;
867
+ const [editing, setEditing] = createSignal(false);
868
+ const [executing, setExecuting] = createSignal(false);
869
+ const [content, setContent] = createSignal(props.planContent);
870
+ let textareaRef;
871
+ const handleSave = () => {
872
+ const text = textareaRef?.plainText ?? content();
873
+ const saved = writePlan(props.projectId, props.sessionId, text);
874
+ props.api.ui.toast({
875
+ message: saved ? "Plan saved" : "Failed to save plan",
876
+ variant: saved ? "success" : "error",
877
+ duration: 3000
878
+ });
879
+ if (saved) {
880
+ setContent(text);
881
+ setEditing(false);
882
+ }
883
+ };
884
+ const handleExport = () => {
885
+ const planText = content();
886
+ const title = extractPlanTitle(planText);
887
+ const slugifiedTitle = slugify(title);
888
+ const directory = props.api.state.path.directory;
889
+ const filename = `${slugifiedTitle}.md`;
890
+ const filepath = join6(directory, filename);
891
+ try {
892
+ writeFileSync(filepath, planText, "utf-8");
893
+ props.api.ui.toast({
894
+ message: `Exported plan to ${filename}`,
895
+ variant: "success",
896
+ duration: 3000
897
+ });
898
+ } catch (error) {
899
+ props.api.ui.toast({
900
+ message: `Failed to export plan: ${error.message}`,
901
+ variant: "error",
902
+ duration: 3000
903
+ });
904
+ }
905
+ };
906
+ function getModeDescription(label) {
907
+ switch (label) {
908
+ case "New session":
909
+ return "Create a new session and send the plan to the code agent";
910
+ case "Execute here":
911
+ return "Execute the plan in the current session using the code agent";
912
+ case "Loop (worktree)":
913
+ return "Execute using iterative development loop in an isolated git worktree";
914
+ case "Loop":
915
+ return "Execute using iterative development loop in the current directory";
916
+ default:
917
+ return "";
918
+ }
919
+ }
920
+ const handleExecuteMode = async (mode) => {
921
+ const planText = content();
922
+ const title = extractPlanTitle(planText);
923
+ const directory = props.api.state.path.directory;
924
+ const pid = resolveProjectId(directory);
925
+ if (!pid) {
926
+ props.api.ui.toast({
927
+ message: "Failed to resolve project ID",
928
+ variant: "error",
929
+ duration: 3000
930
+ });
931
+ return;
932
+ }
933
+ const matchedLabel = matchExecutionLabel(mode);
934
+ switch (matchedLabel) {
935
+ case "New session": {
936
+ props.api.ui.dialog.clear();
937
+ props.api.ui.toast({
938
+ message: "Creating new session for plan execution...",
939
+ variant: "info",
940
+ duration: 3000
941
+ });
942
+ try {
943
+ const createResult = await props.api.client.session.create({
944
+ title,
945
+ directory
946
+ });
947
+ if (createResult.error || !createResult.data) {
948
+ props.api.ui.toast({
949
+ message: "Failed to create new session",
950
+ variant: "error",
951
+ duration: 3000
952
+ });
953
+ return;
954
+ }
955
+ const newSessionId = createResult.data.id;
956
+ if (pid) {
957
+ deletePlan(pid, props.sessionId);
958
+ }
959
+ await props.api.client.session.promptAsync({
960
+ sessionID: newSessionId,
961
+ directory,
962
+ agent: "code",
963
+ parts: [{
964
+ type: "text",
965
+ text: planText
966
+ }]
967
+ });
968
+ props.api.ui.toast({
969
+ message: `New session created: ${title}`,
970
+ variant: "success",
971
+ duration: 3000
972
+ });
973
+ props.onRefresh?.();
974
+ try {
975
+ props.api.route.navigate("session", {
976
+ sessionID: newSessionId
977
+ });
978
+ } catch {}
979
+ } catch {
980
+ props.api.ui.toast({
981
+ message: "Failed to create new session",
982
+ variant: "error",
983
+ duration: 3000
984
+ });
985
+ }
986
+ break;
987
+ }
988
+ case "Execute here": {
989
+ props.api.ui.dialog.clear();
990
+ props.api.ui.toast({
991
+ message: "Switching to code agent for plan execution...",
992
+ variant: "info",
993
+ duration: 3000
994
+ });
995
+ const inPlacePrompt = `The architect agent has created an implementation plan. You are now the code agent taking over this session. Your job is to execute the plan — edit files, run commands, create tests, and implement every phase. Do NOT just describe or summarize the changes. Actually make them.
996
+
997
+ Implementation Plan:
998
+ ${planText}`;
999
+ try {
1000
+ await props.api.client.session.promptAsync({
1001
+ sessionID: props.sessionId,
1002
+ directory,
1003
+ agent: "code",
1004
+ parts: [{
1005
+ type: "text",
1006
+ text: inPlacePrompt
1007
+ }]
1008
+ });
1009
+ props.api.ui.toast({
1010
+ message: "Executing plan in current session",
1011
+ variant: "success",
1012
+ duration: 3000
1013
+ });
1014
+ props.onRefresh?.();
1015
+ } catch {
1016
+ props.api.ui.toast({
1017
+ message: "Failed to execute plan in current session",
1018
+ variant: "error",
1019
+ duration: 3000
1020
+ });
1021
+ }
1022
+ break;
1023
+ }
1024
+ case "Loop (worktree)":
1025
+ case "Loop": {
1026
+ const isWorktree = matchedLabel === "Loop (worktree)";
1027
+ props.api.ui.dialog.clear();
1028
+ props.api.ui.toast({
1029
+ message: isWorktree ? "Starting loop in worktree..." : "Starting loop in-place...",
1030
+ variant: "info",
1031
+ duration: 3000
1032
+ });
1033
+ try {
1034
+ const launchResult = await launchFreshLoop({
1035
+ planText,
1036
+ title,
1037
+ directory,
1038
+ projectId: pid,
1039
+ isWorktree,
1040
+ api: props.api
1041
+ });
1042
+ if (launchResult) {
1043
+ if (pid) {
1044
+ deletePlan(pid, props.sessionId);
1045
+ }
1046
+ props.api.ui.toast({
1047
+ message: isWorktree ? `Loop started in worktree: ${launchResult.loopName}` : `Loop started: ${launchResult.loopName}`,
1048
+ variant: "success",
1049
+ duration: 3000
1050
+ });
1051
+ props.onRefresh?.();
1052
+ }
1053
+ } catch {
1054
+ props.api.ui.toast({
1055
+ message: "Failed to start loop",
1056
+ variant: "error",
1057
+ duration: 3000
1058
+ });
1059
+ }
1060
+ break;
1061
+ }
1062
+ default: {
1063
+ props.api.ui.toast({
1064
+ message: "Unknown execution mode",
1065
+ variant: "error",
1066
+ duration: 3000
1067
+ });
1068
+ }
1069
+ }
1070
+ };
1071
+ return (() => {
1072
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("b"), _el$6 = _$createElement("text"), _el$8 = _$createElement("text"), _el$0 = _$createElement("text"), _el$10 = _$createElement("text"), _el$21 = _$createElement("box"), _el$26 = _$createElement("text");
1073
+ _$insertNode(_el$, _el$2);
1074
+ _$insertNode(_el$, _el$21);
1075
+ _$setProp(_el$, "flexDirection", "column");
1076
+ _$setProp(_el$, "paddingX", 2);
1077
+ _$insertNode(_el$2, _el$3);
1078
+ _$insertNode(_el$2, _el$6);
1079
+ _$insertNode(_el$2, _el$8);
1080
+ _$insertNode(_el$2, _el$0);
1081
+ _$insertNode(_el$2, _el$10);
1082
+ _$setProp(_el$2, "flexShrink", 0);
1083
+ _$setProp(_el$2, "paddingBottom", 1);
1084
+ _$setProp(_el$2, "flexDirection", "row");
1085
+ _$setProp(_el$2, "gap", 2);
1086
+ _$insertNode(_el$3, _el$4);
1087
+ _$insertNode(_el$4, _$createTextNode(`Plan`));
1088
+ _$insertNode(_el$6, _$createTextNode(`[view]`));
1089
+ _$setProp(_el$6, "onMouseUp", () => {
1090
+ setEditing(false);
1091
+ setExecuting(false);
1092
+ });
1093
+ _$insertNode(_el$8, _$createTextNode(`[edit]`));
1094
+ _$setProp(_el$8, "onMouseUp", () => {
1095
+ setEditing(true);
1096
+ setExecuting(false);
1097
+ });
1098
+ _$insertNode(_el$0, _$createTextNode(`[execute]`));
1099
+ _$setProp(_el$0, "onMouseUp", () => {
1100
+ setEditing(false);
1101
+ setExecuting(true);
1102
+ });
1103
+ _$insertNode(_el$10, _$createTextNode(`[export]`));
1104
+ _$setProp(_el$10, "onMouseUp", handleExport);
1105
+ _$insert(_el$, _$createComponent(Show, {
1106
+ get when() {
1107
+ return _$memo(() => !!!editing())() && !executing();
1108
+ },
1109
+ get children() {
1110
+ var _el$12 = _$createElement("scrollbox"), _el$13 = _$createElement("markdown");
1111
+ _$insertNode(_el$12, _el$13);
1112
+ _$setProp(_el$12, "minHeight", 20);
1113
+ _$setProp(_el$12, "maxHeight", "75%");
1114
+ _$setProp(_el$12, "borderStyle", "rounded");
1115
+ _$setProp(_el$12, "paddingX", 1);
1116
+ _$effect((_p$) => {
1117
+ var _v$ = theme().border, _v$2 = content(), _v$3 = SyntaxStyle.create(), _v$4 = theme().markdownText;
1118
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$12, "borderColor", _v$, _p$.e));
1119
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$13, "content", _v$2, _p$.t));
1120
+ _v$3 !== _p$.a && (_p$.a = _$setProp(_el$13, "syntaxStyle", _v$3, _p$.a));
1121
+ _v$4 !== _p$.o && (_p$.o = _$setProp(_el$13, "fg", _v$4, _p$.o));
1122
+ return _p$;
1123
+ }, {
1124
+ e: undefined,
1125
+ t: undefined,
1126
+ a: undefined,
1127
+ o: undefined
1128
+ });
1129
+ return _el$12;
1130
+ }
1131
+ }), _el$21);
1132
+ _$insert(_el$, _$createComponent(Show, {
1133
+ get when() {
1134
+ return editing();
1135
+ },
1136
+ get children() {
1137
+ var _el$14 = _$createElement("textarea");
1138
+ _$use((value) => {
1139
+ textareaRef = value;
1140
+ }, _el$14);
1141
+ _$setProp(_el$14, "focused", true);
1142
+ _$setProp(_el$14, "minHeight", 20);
1143
+ _$setProp(_el$14, "maxHeight", "75%");
1144
+ _$setProp(_el$14, "paddingX", 1);
1145
+ _$effect((_$p) => _$setProp(_el$14, "initialValue", content(), _$p));
1146
+ return _el$14;
1147
+ }
1148
+ }), _el$21);
1149
+ _$insert(_el$, _$createComponent(Show, {
1150
+ get when() {
1151
+ return executing();
1152
+ },
1153
+ get children() {
1154
+ var _el$15 = _$createElement("box"), _el$16 = _$createElement("box"), _el$17 = _$createElement("text"), _el$18 = _$createElement("b"), _el$20 = _$createElement("select");
1155
+ _$insertNode(_el$15, _el$16);
1156
+ _$insertNode(_el$15, _el$20);
1157
+ _$setProp(_el$15, "flexDirection", "column");
1158
+ _$setProp(_el$15, "paddingBottom", 1);
1159
+ _$setProp(_el$15, "gap", 1);
1160
+ _$setProp(_el$15, "minHeight", 20);
1161
+ _$setProp(_el$15, "maxHeight", "75%");
1162
+ _$insertNode(_el$16, _el$17);
1163
+ _$setProp(_el$16, "paddingBottom", 1);
1164
+ _$insertNode(_el$17, _el$18);
1165
+ _$insertNode(_el$18, _$createTextNode(`Select Execution Mode`));
1166
+ _$setProp(_el$20, "focused", true);
1167
+ _$setProp(_el$20, "onSelect", (_, option) => {
1168
+ if (option?.value) {
1169
+ handleExecuteMode(option.value);
1170
+ }
1171
+ });
1172
+ _$setProp(_el$20, "showDescription", false);
1173
+ _$setProp(_el$20, "itemSpacing", 1);
1174
+ _$setProp(_el$20, "wrapSelection", true);
1175
+ _$setProp(_el$20, "selectedTextColor", "#ffffff");
1176
+ _$setProp(_el$20, "minHeight", 12);
1177
+ _$setProp(_el$20, "flexGrow", 1);
1178
+ _$effect((_p$) => {
1179
+ var _v$5 = theme().text, _v$6 = PLAN_EXECUTION_LABELS.map((label) => ({
1180
+ name: label,
1181
+ description: getModeDescription(label),
1182
+ value: label
1183
+ })), _v$7 = theme().text, _v$8 = theme().text, _v$9 = theme().borderActive;
1184
+ _v$5 !== _p$.e && (_p$.e = _$setProp(_el$17, "fg", _v$5, _p$.e));
1185
+ _v$6 !== _p$.t && (_p$.t = _$setProp(_el$20, "options", _v$6, _p$.t));
1186
+ _v$7 !== _p$.a && (_p$.a = _$setProp(_el$20, "textColor", _v$7, _p$.a));
1187
+ _v$8 !== _p$.o && (_p$.o = _$setProp(_el$20, "focusedTextColor", _v$8, _p$.o));
1188
+ _v$9 !== _p$.i && (_p$.i = _$setProp(_el$20, "selectedBackgroundColor", _v$9, _p$.i));
1189
+ return _p$;
1190
+ }, {
1191
+ e: undefined,
1192
+ t: undefined,
1193
+ a: undefined,
1194
+ o: undefined,
1195
+ i: undefined
1196
+ });
1197
+ return _el$15;
1198
+ }
1199
+ }), _el$21);
1200
+ _$insertNode(_el$21, _el$26);
1201
+ _$setProp(_el$21, "paddingTop", 1);
1202
+ _$setProp(_el$21, "flexShrink", 0);
1203
+ _$setProp(_el$21, "flexDirection", "row");
1204
+ _$setProp(_el$21, "gap", 2);
1205
+ _$insert(_el$21, _$createComponent(Show, {
1206
+ get when() {
1207
+ return editing();
1208
+ },
1209
+ get children() {
1210
+ var _el$22 = _$createElement("text");
1211
+ _$insertNode(_el$22, _$createTextNode(`Save`));
1212
+ _$setProp(_el$22, "onMouseUp", handleSave);
1213
+ _$effect((_$p) => _$setProp(_el$22, "fg", theme().success, _$p));
1214
+ return _el$22;
1215
+ }
1216
+ }), _el$26);
1217
+ _$insert(_el$21, _$createComponent(Show, {
1218
+ get when() {
1219
+ return executing();
1220
+ },
1221
+ get children() {
1222
+ var _el$24 = _$createElement("text");
1223
+ _$insertNode(_el$24, _$createTextNode(`Back to plan`));
1224
+ _$setProp(_el$24, "onMouseUp", () => setExecuting(false));
1225
+ _$effect((_$p) => _$setProp(_el$24, "fg", theme().textMuted, _$p));
1226
+ return _el$24;
1227
+ }
1228
+ }), _el$26);
1229
+ _$insertNode(_el$26, _$createTextNode(`Close (esc)`));
1230
+ _$setProp(_el$26, "onMouseUp", () => props.api.ui.dialog.clear());
1231
+ _$effect((_p$) => {
1232
+ var _v$0 = theme().text, _v$1 = executing() ? theme().textMuted : editing() ? theme().text : theme().info, _v$10 = editing() ? theme().text : theme().textMuted, _v$11 = executing() ? theme().text : theme().textMuted, _v$12 = theme().textMuted, _v$13 = theme().textMuted;
1233
+ _v$0 !== _p$.e && (_p$.e = _$setProp(_el$3, "fg", _v$0, _p$.e));
1234
+ _v$1 !== _p$.t && (_p$.t = _$setProp(_el$6, "fg", _v$1, _p$.t));
1235
+ _v$10 !== _p$.a && (_p$.a = _$setProp(_el$8, "fg", _v$10, _p$.a));
1236
+ _v$11 !== _p$.o && (_p$.o = _$setProp(_el$0, "fg", _v$11, _p$.o));
1237
+ _v$12 !== _p$.i && (_p$.i = _$setProp(_el$10, "fg", _v$12, _p$.i));
1238
+ _v$13 !== _p$.n && (_p$.n = _$setProp(_el$26, "fg", _v$13, _p$.n));
1239
+ return _p$;
1240
+ }, {
1241
+ e: undefined,
1242
+ t: undefined,
1243
+ a: undefined,
1244
+ o: undefined,
1245
+ i: undefined,
1246
+ n: undefined
1247
+ });
1248
+ return _el$;
1249
+ })();
1250
+ }
1251
+ function LoopDetailsDialog(props) {
1252
+ const theme = () => props.api.theme.current;
1253
+ const [currentLoop, setCurrentLoop] = createSignal(props.loop);
1254
+ const [stats, setStats] = createSignal(null);
1255
+ const [loading, setLoading] = createSignal(true);
1256
+ const directory = props.api.state.path.directory;
1257
+ const pid = resolveProjectId(directory);
1258
+ const refreshLoopState = () => {
1259
+ if (pid && currentLoop().name) {
1260
+ const freshLoop = readLoopByName(pid, currentLoop().name);
1261
+ if (freshLoop) {
1262
+ setCurrentLoop(freshLoop);
1263
+ }
1264
+ }
1265
+ };
1266
+ refreshLoopState();
1267
+ createEffect(() => {
1268
+ const loop = currentLoop();
1269
+ if (loop.sessionId && directory) {
1270
+ setLoading(true);
1271
+ fetchSessionStats(props.api, loop.sessionId, directory).then((result) => {
1272
+ setStats(result);
1273
+ setLoading(false);
1274
+ }).catch(() => {
1275
+ setStats(null);
1276
+ setLoading(false);
1277
+ });
1278
+ } else {
1279
+ setLoading(false);
1280
+ }
1281
+ });
1282
+ const handleCancel = () => {
1283
+ props.api.ui.dialog.clear();
1284
+ const directory2 = props.api.state.path.directory;
1285
+ const pid2 = resolveProjectId(directory2);
1286
+ if (!pid2)
1287
+ return;
1288
+ const sessionId = cancelLoop(pid2, currentLoop().name);
1289
+ if (sessionId) {
1290
+ props.api.client.session.abort({
1291
+ sessionID: sessionId
1292
+ }).catch(() => {});
1293
+ }
1294
+ props.api.ui.toast({
1295
+ message: sessionId ? `Cancelled loop: ${currentLoop().name}` : `Loop ${currentLoop().name} is not active`,
1296
+ variant: sessionId ? "success" : "info",
1297
+ duration: 3000
1298
+ });
1299
+ props.onRefresh?.();
1300
+ };
1301
+ const handleRestart = async () => {
1302
+ props.api.ui.dialog.clear();
1303
+ const directory2 = props.api.state.path.directory;
1304
+ const pid2 = resolveProjectId(directory2);
1305
+ if (!pid2)
1306
+ return;
1307
+ const newSessionId = await restartLoop(pid2, currentLoop().name, props.api);
1308
+ const label = currentLoop().active ? "Force restarting" : "Restarting";
1309
+ props.api.ui.toast({
1310
+ message: newSessionId ? `${label} loop: ${currentLoop().name}` : `Failed to restart loop: ${currentLoop().name}`,
1311
+ variant: newSessionId ? "success" : "error",
1312
+ duration: 3000
1313
+ });
1314
+ props.onRefresh?.();
1315
+ };
1316
+ const statusBadge = () => {
1317
+ const loop = currentLoop();
1318
+ if (loop.active)
1319
+ return {
1320
+ text: loop.phase,
1321
+ color: loop.phase === "auditing" ? theme().warning : theme().success
1322
+ };
1323
+ if (loop.terminationReason === "completed")
1324
+ return {
1325
+ text: "completed",
1326
+ color: theme().success
1327
+ };
1328
+ if (loop.terminationReason === "cancelled" || loop.terminationReason === "user_aborted")
1329
+ return {
1330
+ text: "cancelled",
1331
+ color: theme().textMuted
1332
+ };
1333
+ return {
1334
+ text: "ended",
1335
+ color: theme().error
1336
+ };
1337
+ };
1338
+ return (() => {
1339
+ var _el$28 = _$createElement("box"), _el$29 = _$createElement("box"), _el$30 = _$createElement("box"), _el$31 = _$createElement("text"), _el$32 = _$createElement("b"), _el$33 = _$createElement("text"), _el$34 = _$createElement("b"), _el$35 = _$createTextNode(`[`), _el$36 = _$createTextNode(`]`), _el$37 = _$createElement("box"), _el$38 = _$createElement("text"), _el$39 = _$createTextNode(`Iteration `), _el$90 = _$createElement("box"), _el$99 = _$createElement("text");
1340
+ _$insertNode(_el$28, _el$29);
1341
+ _$insertNode(_el$28, _el$90);
1342
+ _$setProp(_el$28, "flexDirection", "column");
1343
+ _$setProp(_el$28, "paddingX", 2);
1344
+ _$insertNode(_el$29, _el$30);
1345
+ _$insertNode(_el$29, _el$37);
1346
+ _$setProp(_el$29, "flexDirection", "column");
1347
+ _$setProp(_el$29, "flexShrink", 0);
1348
+ _$insertNode(_el$30, _el$31);
1349
+ _$insertNode(_el$30, _el$33);
1350
+ _$setProp(_el$30, "flexDirection", "row");
1351
+ _$setProp(_el$30, "gap", 1);
1352
+ _$setProp(_el$30, "alignItems", "center");
1353
+ _$insertNode(_el$31, _el$32);
1354
+ _$insert(_el$32, () => currentLoop().name);
1355
+ _$insertNode(_el$33, _el$34);
1356
+ _$insertNode(_el$34, _el$35);
1357
+ _$insertNode(_el$34, _el$36);
1358
+ _$insert(_el$34, () => statusBadge().text, _el$36);
1359
+ _$insertNode(_el$37, _el$38);
1360
+ _$insertNode(_el$38, _el$39);
1361
+ _$insert(_el$38, () => currentLoop().iteration, null);
1362
+ _$insert(_el$38, (() => {
1363
+ var _c$ = _$memo(() => currentLoop().maxIterations > 0);
1364
+ return () => _c$() ? `/${currentLoop().maxIterations}` : "";
1365
+ })(), null);
1366
+ _$insert(_el$28, _$createComponent(Show, {
1367
+ get when() {
1368
+ return loading();
1369
+ },
1370
+ get children() {
1371
+ var _el$40 = _$createElement("box"), _el$41 = _$createElement("text");
1372
+ _$insertNode(_el$40, _el$41);
1373
+ _$setProp(_el$40, "paddingTop", 1);
1374
+ _$insertNode(_el$41, _$createTextNode(`Loading stats...`));
1375
+ _$effect((_$p) => _$setProp(_el$41, "fg", theme().textMuted, _$p));
1376
+ return _el$40;
1377
+ }
1378
+ }), _el$90);
1379
+ _$insert(_el$28, _$createComponent(Show, {
1380
+ get when() {
1381
+ return !loading();
1382
+ },
1383
+ get children() {
1384
+ var _el$43 = _$createElement("box");
1385
+ _$setProp(_el$43, "flexDirection", "column");
1386
+ _$setProp(_el$43, "paddingTop", 1);
1387
+ _$setProp(_el$43, "flexShrink", 0);
1388
+ _$insert(_el$43, _$createComponent(Show, {
1389
+ get when() {
1390
+ return stats();
1391
+ },
1392
+ get fallback() {
1393
+ return (() => {
1394
+ var _el$101 = _$createElement("box"), _el$102 = _$createElement("text");
1395
+ _$insertNode(_el$101, _el$102);
1396
+ _$insertNode(_el$102, _$createTextNode(`Session stats unavailable`));
1397
+ _$effect((_$p) => _$setProp(_el$102, "fg", theme().textMuted, _$p));
1398
+ return _el$101;
1399
+ })();
1400
+ },
1401
+ get children() {
1402
+ var _el$44 = _$createElement("box"), _el$45 = _$createElement("box"), _el$46 = _$createElement("text"), _el$47 = _$createElement("span"), _el$49 = _$createTextNode(`...`), _el$50 = _$createElement("box"), _el$51 = _$createElement("text"), _el$52 = _$createElement("span"), _el$54 = _$createElement("box"), _el$55 = _$createElement("text"), _el$56 = _$createElement("span"), _el$58 = _$createTextNode(` total (`), _el$59 = _$createTextNode(` assistant)`), _el$60 = _$createElement("box"), _el$61 = _$createElement("text"), _el$62 = _$createElement("span"), _el$64 = _$createTextNode(` in / `), _el$65 = _$createTextNode(` out / `), _el$66 = _$createTextNode(` reasoning`), _el$67 = _$createElement("box"), _el$68 = _$createElement("text"), _el$69 = _$createElement("span"), _el$71 = _$createTextNode(`$`);
1403
+ _$insertNode(_el$44, _el$45);
1404
+ _$insertNode(_el$44, _el$50);
1405
+ _$insertNode(_el$44, _el$54);
1406
+ _$insertNode(_el$44, _el$60);
1407
+ _$insertNode(_el$44, _el$67);
1408
+ _$setProp(_el$44, "flexDirection", "column");
1409
+ _$insertNode(_el$45, _el$46);
1410
+ _$insertNode(_el$46, _el$47);
1411
+ _$insertNode(_el$46, _el$49);
1412
+ _$insertNode(_el$47, _$createTextNode(`Session: `));
1413
+ _$insert(_el$46, () => currentLoop().sessionId.slice(0, 8), _el$49);
1414
+ _$insertNode(_el$50, _el$51);
1415
+ _$insertNode(_el$51, _el$52);
1416
+ _$insertNode(_el$52, _$createTextNode(`Phase: `));
1417
+ _$insert(_el$51, () => currentLoop().phase, null);
1418
+ _$insertNode(_el$54, _el$55);
1419
+ _$insertNode(_el$55, _el$56);
1420
+ _$insertNode(_el$55, _el$58);
1421
+ _$insertNode(_el$55, _el$59);
1422
+ _$insertNode(_el$56, _$createTextNode(`Messages: `));
1423
+ _$insert(_el$55, () => stats().messages.total, _el$58);
1424
+ _$insert(_el$55, () => stats().messages.assistant, _el$59);
1425
+ _$insertNode(_el$60, _el$61);
1426
+ _$insertNode(_el$61, _el$62);
1427
+ _$insertNode(_el$61, _el$64);
1428
+ _$insertNode(_el$61, _el$65);
1429
+ _$insertNode(_el$61, _el$66);
1430
+ _$insertNode(_el$62, _$createTextNode(`Tokens: `));
1431
+ _$insert(_el$61, () => formatTokens(stats().tokens.input), _el$64);
1432
+ _$insert(_el$61, () => formatTokens(stats().tokens.output), _el$65);
1433
+ _$insert(_el$61, () => formatTokens(stats().tokens.reasoning), _el$66);
1434
+ _$insertNode(_el$67, _el$68);
1435
+ _$insertNode(_el$68, _el$69);
1436
+ _$insertNode(_el$68, _el$71);
1437
+ _$insertNode(_el$69, _$createTextNode(`Cost: `));
1438
+ _$insert(_el$68, () => stats().cost.toFixed(4), null);
1439
+ _$insert(_el$44, _$createComponent(Show, {
1440
+ get when() {
1441
+ return stats().fileChanges;
1442
+ },
1443
+ get children() {
1444
+ var _el$72 = _$createElement("box"), _el$73 = _$createElement("text"), _el$74 = _$createElement("span"), _el$76 = _$createTextNode(` changed (+`), _el$77 = _$createTextNode(`/-`), _el$78 = _$createTextNode(`)`);
1445
+ _$insertNode(_el$72, _el$73);
1446
+ _$insertNode(_el$73, _el$74);
1447
+ _$insertNode(_el$73, _el$76);
1448
+ _$insertNode(_el$73, _el$77);
1449
+ _$insertNode(_el$73, _el$78);
1450
+ _$insertNode(_el$74, _$createTextNode(`Files: `));
1451
+ _$insert(_el$73, () => stats().fileChanges.files, _el$76);
1452
+ _$insert(_el$73, () => stats().fileChanges.additions, _el$77);
1453
+ _$insert(_el$73, () => stats().fileChanges.deletions, _el$78);
1454
+ _$effect((_p$) => {
1455
+ var _v$14 = theme().text, _v$15 = {
1456
+ fg: theme().textMuted
1457
+ };
1458
+ _v$14 !== _p$.e && (_p$.e = _$setProp(_el$73, "fg", _v$14, _p$.e));
1459
+ _v$15 !== _p$.t && (_p$.t = _$setProp(_el$74, "style", _v$15, _p$.t));
1460
+ return _p$;
1461
+ }, {
1462
+ e: undefined,
1463
+ t: undefined
1464
+ });
1465
+ return _el$72;
1466
+ }
1467
+ }), null);
1468
+ _$insert(_el$44, _$createComponent(Show, {
1469
+ get when() {
1470
+ return stats().timing;
1471
+ },
1472
+ get children() {
1473
+ var _el$79 = _$createElement("box"), _el$80 = _$createElement("text"), _el$81 = _$createElement("span");
1474
+ _$insertNode(_el$79, _el$80);
1475
+ _$insertNode(_el$80, _el$81);
1476
+ _$insertNode(_el$81, _$createTextNode(`Duration: `));
1477
+ _$insert(_el$80, () => formatDuration(stats().timing.durationMs), null);
1478
+ _$effect((_p$) => {
1479
+ var _v$16 = theme().text, _v$17 = {
1480
+ fg: theme().textMuted
1481
+ };
1482
+ _v$16 !== _p$.e && (_p$.e = _$setProp(_el$80, "fg", _v$16, _p$.e));
1483
+ _v$17 !== _p$.t && (_p$.t = _$setProp(_el$81, "style", _v$17, _p$.t));
1484
+ return _p$;
1485
+ }, {
1486
+ e: undefined,
1487
+ t: undefined
1488
+ });
1489
+ return _el$79;
1490
+ }
1491
+ }), null);
1492
+ _$effect((_p$) => {
1493
+ var _v$18 = theme().text, _v$19 = {
1494
+ fg: theme().textMuted
1495
+ }, _v$20 = theme().text, _v$21 = {
1496
+ fg: theme().textMuted
1497
+ }, _v$22 = theme().text, _v$23 = {
1498
+ fg: theme().textMuted
1499
+ }, _v$24 = theme().text, _v$25 = {
1500
+ fg: theme().textMuted
1501
+ }, _v$26 = theme().text, _v$27 = {
1502
+ fg: theme().textMuted
1503
+ };
1504
+ _v$18 !== _p$.e && (_p$.e = _$setProp(_el$46, "fg", _v$18, _p$.e));
1505
+ _v$19 !== _p$.t && (_p$.t = _$setProp(_el$47, "style", _v$19, _p$.t));
1506
+ _v$20 !== _p$.a && (_p$.a = _$setProp(_el$51, "fg", _v$20, _p$.a));
1507
+ _v$21 !== _p$.o && (_p$.o = _$setProp(_el$52, "style", _v$21, _p$.o));
1508
+ _v$22 !== _p$.i && (_p$.i = _$setProp(_el$55, "fg", _v$22, _p$.i));
1509
+ _v$23 !== _p$.n && (_p$.n = _$setProp(_el$56, "style", _v$23, _p$.n));
1510
+ _v$24 !== _p$.s && (_p$.s = _$setProp(_el$61, "fg", _v$24, _p$.s));
1511
+ _v$25 !== _p$.h && (_p$.h = _$setProp(_el$62, "style", _v$25, _p$.h));
1512
+ _v$26 !== _p$.r && (_p$.r = _$setProp(_el$68, "fg", _v$26, _p$.r));
1513
+ _v$27 !== _p$.d && (_p$.d = _$setProp(_el$69, "style", _v$27, _p$.d));
1514
+ return _p$;
1515
+ }, {
1516
+ e: undefined,
1517
+ t: undefined,
1518
+ a: undefined,
1519
+ o: undefined,
1520
+ i: undefined,
1521
+ n: undefined,
1522
+ s: undefined,
1523
+ h: undefined,
1524
+ r: undefined,
1525
+ d: undefined
1526
+ });
1527
+ return _el$44;
1528
+ }
1529
+ }));
1530
+ return _el$43;
1531
+ }
1532
+ }), _el$90);
1533
+ _$insert(_el$28, _$createComponent(Show, {
1534
+ get when() {
1535
+ return stats()?.lastActivity?.summary;
1536
+ },
1537
+ get children() {
1538
+ var _el$83 = _$createElement("box"), _el$84 = _$createElement("box"), _el$85 = _$createElement("text"), _el$86 = _$createElement("b"), _el$88 = _$createElement("scrollbox"), _el$89 = _$createElement("text");
1539
+ _$insertNode(_el$83, _el$84);
1540
+ _$insertNode(_el$83, _el$88);
1541
+ _$setProp(_el$83, "flexDirection", "column");
1542
+ _$setProp(_el$83, "paddingTop", 1);
1543
+ _$setProp(_el$83, "flexGrow", 1);
1544
+ _$setProp(_el$83, "flexShrink", 1);
1545
+ _$insertNode(_el$84, _el$85);
1546
+ _$setProp(_el$84, "flexShrink", 0);
1547
+ _$insertNode(_el$85, _el$86);
1548
+ _$insertNode(_el$86, _$createTextNode(`Latest Output`));
1549
+ _$insertNode(_el$88, _el$89);
1550
+ _$setProp(_el$88, "maxHeight", 12);
1551
+ _$setProp(_el$88, "borderStyle", "rounded");
1552
+ _$setProp(_el$88, "paddingX", 1);
1553
+ _$setProp(_el$89, "wrapMode", "word");
1554
+ _$insert(_el$89, () => truncate(stats().lastActivity.summary, 500));
1555
+ _$effect((_p$) => {
1556
+ var _v$28 = theme().text, _v$29 = theme().border, _v$30 = theme().textMuted;
1557
+ _v$28 !== _p$.e && (_p$.e = _$setProp(_el$85, "fg", _v$28, _p$.e));
1558
+ _v$29 !== _p$.t && (_p$.t = _$setProp(_el$88, "borderColor", _v$29, _p$.t));
1559
+ _v$30 !== _p$.a && (_p$.a = _$setProp(_el$89, "fg", _v$30, _p$.a));
1560
+ return _p$;
1561
+ }, {
1562
+ e: undefined,
1563
+ t: undefined,
1564
+ a: undefined
1565
+ });
1566
+ return _el$83;
1567
+ }
1568
+ }), _el$90);
1569
+ _$insertNode(_el$90, _el$99);
1570
+ _$setProp(_el$90, "paddingTop", 1);
1571
+ _$setProp(_el$90, "flexShrink", 0);
1572
+ _$setProp(_el$90, "flexDirection", "row");
1573
+ _$setProp(_el$90, "gap", 2);
1574
+ _$setProp(_el$90, "paddingY", 2);
1575
+ _$insert(_el$90, _$createComponent(Show, {
1576
+ get when() {
1577
+ return props.onBack;
1578
+ },
1579
+ get children() {
1580
+ var _el$91 = _$createElement("text");
1581
+ _$insertNode(_el$91, _$createTextNode(`Back`));
1582
+ _$setProp(_el$91, "onMouseUp", () => props.onBack());
1583
+ _$effect((_$p) => _$setProp(_el$91, "fg", theme().textMuted, _$p));
1584
+ return _el$91;
1585
+ }
1586
+ }), _el$99);
1587
+ _$insert(_el$90, _$createComponent(Show, {
1588
+ get when() {
1589
+ return currentLoop().active;
1590
+ },
1591
+ get children() {
1592
+ return [(() => {
1593
+ var _el$93 = _$createElement("text");
1594
+ _$insertNode(_el$93, _$createTextNode(`Force Restart`));
1595
+ _$setProp(_el$93, "onMouseUp", handleRestart);
1596
+ _$effect((_$p) => _$setProp(_el$93, "fg", theme().warning, _$p));
1597
+ return _el$93;
1598
+ })(), (() => {
1599
+ var _el$95 = _$createElement("text");
1600
+ _$insertNode(_el$95, _$createTextNode(`Cancel loop`));
1601
+ _$setProp(_el$95, "onMouseUp", handleCancel);
1602
+ _$effect((_$p) => _$setProp(_el$95, "fg", theme().error, _$p));
1603
+ return _el$95;
1604
+ })()];
1605
+ }
1606
+ }), _el$99);
1607
+ _$insert(_el$90, _$createComponent(Show, {
1608
+ get when() {
1609
+ return _$memo(() => !!!currentLoop().active)() && currentLoop().terminationReason !== "completed";
1610
+ },
1611
+ get children() {
1612
+ var _el$97 = _$createElement("text");
1613
+ _$insertNode(_el$97, _$createTextNode(`Restart`));
1614
+ _$setProp(_el$97, "onMouseUp", handleRestart);
1615
+ _$effect((_$p) => _$setProp(_el$97, "fg", theme().success, _$p));
1616
+ return _el$97;
1617
+ }
1618
+ }), _el$99);
1619
+ _$insertNode(_el$99, _$createTextNode(`Close (esc)`));
1620
+ _$setProp(_el$99, "onMouseUp", () => props.api.ui.dialog.clear());
1621
+ _$effect((_p$) => {
1622
+ var _v$31 = theme().text, _v$32 = statusBadge().color, _v$33 = theme().textMuted, _v$34 = theme().textMuted;
1623
+ _v$31 !== _p$.e && (_p$.e = _$setProp(_el$31, "fg", _v$31, _p$.e));
1624
+ _v$32 !== _p$.t && (_p$.t = _$setProp(_el$33, "fg", _v$32, _p$.t));
1625
+ _v$33 !== _p$.a && (_p$.a = _$setProp(_el$38, "fg", _v$33, _p$.a));
1626
+ _v$34 !== _p$.o && (_p$.o = _$setProp(_el$99, "fg", _v$34, _p$.o));
1627
+ return _p$;
1628
+ }, {
1629
+ e: undefined,
1630
+ t: undefined,
1631
+ a: undefined,
1632
+ o: undefined
1633
+ });
1634
+ return _el$28;
1635
+ })();
1636
+ }
1637
+ function Sidebar(props) {
1638
+ const [open, setOpen] = createSignal(true);
1639
+ const [loops, setLoops] = createSignal([]);
1640
+ const [hasPlan, setHasPlan] = createSignal(false);
1641
+ const [graphStatusFormatted, setGraphStatusFormatted] = createSignal(null);
1642
+ const theme = () => props.api.theme.current;
1643
+ const directory = props.api.state.path.directory;
1644
+ const pid = resolveProjectId(directory);
1645
+ const title = createMemo(() => {
1646
+ return props.opts.showVersion ? `Graph v${VERSION}` : "Graph";
1647
+ });
1648
+ const dot = (loop) => {
1649
+ if (!loop.active) {
1650
+ if (loop.terminationReason === "completed")
1651
+ return theme().success;
1652
+ if (loop.terminationReason === "cancelled" || loop.terminationReason === "user_aborted")
1653
+ return theme().textMuted;
1654
+ return theme().error;
1655
+ }
1656
+ if (loop.phase === "auditing")
1657
+ return theme().warning;
1658
+ return theme().success;
1659
+ };
1660
+ const statusText = (loop) => {
1661
+ const max = loop.maxIterations > 0 ? `/${loop.maxIterations}` : "";
1662
+ if (loop.active)
1663
+ return `${loop.phase} · iter ${loop.iteration}${max}`;
1664
+ if (loop.terminationReason === "completed")
1665
+ return `completed · ${loop.iteration} iter${loop.iteration !== 1 ? "s" : ""}`;
1666
+ return loop.terminationReason?.replace(/_/g, " ") ?? "ended";
1667
+ };
1668
+ function refreshSidebarData() {
1669
+ if (!pid)
1670
+ return;
1671
+ const states = readLoopStates(pid);
1672
+ const cutoff = Date.now() - 5 * 60 * 1000;
1673
+ const visible = states.filter((l) => l.active || l.completedAt && new Date(l.completedAt).getTime() > cutoff);
1674
+ visible.sort((a, b) => {
1675
+ if (a.active && !b.active)
1676
+ return -1;
1677
+ if (!a.active && b.active)
1678
+ return 1;
1679
+ const aTime = a.completedAt ?? a.startedAt ?? "";
1680
+ const bTime = b.completedAt ?? b.startedAt ?? "";
1681
+ return bTime.localeCompare(aTime);
1682
+ });
1683
+ setLoops(visible);
1684
+ if (props.sessionId) {
1685
+ const plan = readPlan(pid, props.sessionId);
1686
+ setHasPlan(plan !== null);
1687
+ }
1688
+ const status = readGraphStatus(pid);
1689
+ setGraphStatusFormatted(formatGraphStatus(status));
1690
+ }
1691
+ const unsub = props.api.event.on("session.status", () => {
1692
+ refreshSidebarData();
1693
+ });
1694
+ let pollTimer = null;
1695
+ function startPolling() {
1696
+ if (pollTimer)
1697
+ return;
1698
+ pollTimer = setInterval(() => {
1699
+ refreshSidebarData();
1700
+ }, 5000);
1701
+ }
1702
+ function stopPolling() {
1703
+ if (pollTimer) {
1704
+ clearInterval(pollTimer);
1705
+ pollTimer = null;
1706
+ }
1707
+ }
1708
+ refreshSidebarData();
1709
+ createEffect(() => {
1710
+ const hasActiveWorktreeLoops = loops().filter((l) => l.active && l.worktree).length > 0;
1711
+ if (hasActiveWorktreeLoops) {
1712
+ startPolling();
1713
+ } else {
1714
+ stopPolling();
1715
+ }
1716
+ });
1717
+ onCleanup(() => {
1718
+ unsub();
1719
+ stopPolling();
1720
+ });
1721
+ const hasContent = createMemo(() => {
1722
+ if (hasPlan())
1723
+ return true;
1724
+ if (props.opts.showLoops && loops().length > 0)
1725
+ return true;
1726
+ if (graphStatusFormatted())
1727
+ return true;
1728
+ return false;
1729
+ });
1730
+ const activeCount = createMemo(() => {
1731
+ return loops().filter((l) => l.active).length;
1732
+ });
1733
+ return _$createComponent(Show, {
1734
+ get when() {
1735
+ return props.opts.sidebar;
1736
+ },
1737
+ get children() {
1738
+ var _el$104 = _$createElement("box"), _el$105 = _$createElement("box"), _el$107 = _$createElement("text"), _el$108 = _$createElement("b");
1739
+ _$insertNode(_el$104, _el$105);
1740
+ _$insertNode(_el$105, _el$107);
1741
+ _$setProp(_el$105, "flexDirection", "row");
1742
+ _$setProp(_el$105, "gap", 1);
1743
+ _$setProp(_el$105, "onMouseDown", () => hasContent() && setOpen((x) => !x));
1744
+ _$insert(_el$105, _$createComponent(Show, {
1745
+ get when() {
1746
+ return hasContent();
1747
+ },
1748
+ get children() {
1749
+ var _el$106 = _$createElement("text");
1750
+ _$insert(_el$106, () => open() ? "▼" : "▶");
1751
+ _$effect((_$p) => _$setProp(_el$106, "fg", theme().text, _$p));
1752
+ return _el$106;
1753
+ }
1754
+ }), _el$107);
1755
+ _$insertNode(_el$107, _el$108);
1756
+ _$insert(_el$108, title);
1757
+ _$insert(_el$107, (() => {
1758
+ var _c$2 = _$memo(() => !!(!open() && hasPlan()));
1759
+ return () => _c$2() ? (() => {
1760
+ var _el$118 = _$createElement("span");
1761
+ _$insertNode(_el$118, _$createTextNode(` · plan`));
1762
+ _$effect((_$p) => _$setProp(_el$118, "style", {
1763
+ fg: theme().info
1764
+ }, _$p));
1765
+ return _el$118;
1766
+ })() : "";
1767
+ })(), null);
1768
+ _$insert(_el$107, (() => {
1769
+ var _c$3 = _$memo(() => !!(!open() && graphStatusFormatted() && graphStatusFormatted().text.includes("ready")));
1770
+ return () => _c$3() ? (() => {
1771
+ var _el$120 = _$createElement("span");
1772
+ _$insertNode(_el$120, _$createTextNode(` · ready`));
1773
+ _$effect((_$p) => _$setProp(_el$120, "style", {
1774
+ fg: theme().success
1775
+ }, _$p));
1776
+ return _el$120;
1777
+ })() : "";
1778
+ })(), null);
1779
+ _$insert(_el$107, (() => {
1780
+ var _c$4 = _$memo(() => !!(!open() && activeCount() > 0));
1781
+ return () => _c$4() ? (() => {
1782
+ var _el$122 = _$createElement("span");
1783
+ _$insert(_el$122, () => ` (${activeCount()} active)`);
1784
+ _$effect((_$p) => _$setProp(_el$122, "style", {
1785
+ fg: theme().textMuted
1786
+ }, _$p));
1787
+ return _el$122;
1788
+ })() : "";
1789
+ })(), null);
1790
+ _$insert(_el$104, _$createComponent(Show, {
1791
+ get when() {
1792
+ return open();
1793
+ },
1794
+ get children() {
1795
+ return [_$createComponent(Show, {
1796
+ get when() {
1797
+ return hasPlan();
1798
+ },
1799
+ get children() {
1800
+ var _el$109 = _$createElement("box"), _el$110 = _$createElement("text"), _el$112 = _$createElement("text");
1801
+ _$insertNode(_el$109, _el$110);
1802
+ _$insertNode(_el$109, _el$112);
1803
+ _$setProp(_el$109, "flexDirection", "row");
1804
+ _$setProp(_el$109, "gap", 1);
1805
+ _$setProp(_el$109, "onMouseUp", () => {
1806
+ if (!pid || !props.sessionId)
1807
+ return;
1808
+ const plan = readPlan(pid, props.sessionId);
1809
+ if (!plan) {
1810
+ props.api.ui.toast({
1811
+ message: "Plan not found",
1812
+ variant: "info",
1813
+ duration: 3000
1814
+ });
1815
+ return;
1816
+ }
1817
+ const refreshSidebar = refreshSidebarData;
1818
+ props.api.ui.dialog.setSize("xlarge");
1819
+ props.api.ui.dialog.replace(() => _$createComponent(PlanViewerDialog, {
1820
+ get api() {
1821
+ return props.api;
1822
+ },
1823
+ planContent: plan,
1824
+ projectId: pid,
1825
+ get sessionId() {
1826
+ return props.sessionId;
1827
+ },
1828
+ onRefresh: refreshSidebar
1829
+ }));
1830
+ });
1831
+ _$insertNode(_el$110, _$createTextNode(`\uD83D\uDCCB`));
1832
+ _$setProp(_el$110, "flexShrink", 0);
1833
+ _$insertNode(_el$112, _$createTextNode(`Plan`));
1834
+ _$effect((_p$) => {
1835
+ var _v$35 = {
1836
+ fg: theme().info
1837
+ }, _v$36 = theme().text;
1838
+ _v$35 !== _p$.e && (_p$.e = _$setProp(_el$110, "style", _v$35, _p$.e));
1839
+ _v$36 !== _p$.t && (_p$.t = _$setProp(_el$112, "fg", _v$36, _p$.t));
1840
+ return _p$;
1841
+ }, {
1842
+ e: undefined,
1843
+ t: undefined
1844
+ });
1845
+ return _el$109;
1846
+ }
1847
+ }), _$createComponent(Show, {
1848
+ get when() {
1849
+ return graphStatusFormatted();
1850
+ },
1851
+ get children() {
1852
+ var _el$114 = _$createElement("box"), _el$115 = _$createElement("text"), _el$117 = _$createElement("text");
1853
+ _$insertNode(_el$114, _el$115);
1854
+ _$insertNode(_el$114, _el$117);
1855
+ _$setProp(_el$114, "flexDirection", "row");
1856
+ _$setProp(_el$114, "gap", 1);
1857
+ _$insertNode(_el$115, _$createTextNode(`•`));
1858
+ _$setProp(_el$115, "flexShrink", 0);
1859
+ _$setProp(_el$117, "wrapMode", "word");
1860
+ _$insert(_el$117, () => graphStatusFormatted().text);
1861
+ _$effect((_p$) => {
1862
+ var _v$37 = {
1863
+ fg: theme()[graphStatusFormatted().color]
1864
+ }, _v$38 = theme().text;
1865
+ _v$37 !== _p$.e && (_p$.e = _$setProp(_el$115, "style", _v$37, _p$.e));
1866
+ _v$38 !== _p$.t && (_p$.t = _$setProp(_el$117, "fg", _v$38, _p$.t));
1867
+ return _p$;
1868
+ }, {
1869
+ e: undefined,
1870
+ t: undefined
1871
+ });
1872
+ return _el$114;
1873
+ }
1874
+ }), _$createComponent(Show, {
1875
+ get when() {
1876
+ return _$memo(() => !!props.opts.showLoops)() && loops().length > 0;
1877
+ },
1878
+ get children() {
1879
+ return _$createComponent(For, {
1880
+ get each() {
1881
+ return loops();
1882
+ },
1883
+ children: (loop) => (() => {
1884
+ var _el$123 = _$createElement("box"), _el$124 = _$createElement("text"), _el$126 = _$createElement("text"), _el$127 = _$createTextNode(` `), _el$128 = _$createElement("span");
1885
+ _$insertNode(_el$123, _el$124);
1886
+ _$insertNode(_el$123, _el$126);
1887
+ _$setProp(_el$123, "flexDirection", "row");
1888
+ _$setProp(_el$123, "gap", 1);
1889
+ _$setProp(_el$123, "onMouseUp", () => {
1890
+ if (loop.worktree) {
1891
+ props.api.ui.dialog.setSize("medium");
1892
+ props.api.ui.dialog.replace(() => _$createComponent(LoopDetailsDialog, {
1893
+ get api() {
1894
+ return props.api;
1895
+ },
1896
+ loop,
1897
+ onRefresh: refreshSidebarData
1898
+ }));
1899
+ } else {
1900
+ props.api.route.navigate("session", {
1901
+ sessionID: loop.sessionId
1902
+ });
1903
+ }
1904
+ });
1905
+ _$insertNode(_el$124, _$createTextNode(`•`));
1906
+ _$setProp(_el$124, "flexShrink", 0);
1907
+ _$insertNode(_el$126, _el$127);
1908
+ _$insertNode(_el$126, _el$128);
1909
+ _$setProp(_el$126, "wrapMode", "word");
1910
+ _$insert(_el$126, () => truncateMiddle(loop.name, 25), _el$127);
1911
+ _$insert(_el$128, () => statusText(loop));
1912
+ _$effect((_p$) => {
1913
+ var _v$39 = {
1914
+ fg: dot(loop)
1915
+ }, _v$40 = theme().text, _v$41 = {
1916
+ fg: theme().textMuted
1917
+ };
1918
+ _v$39 !== _p$.e && (_p$.e = _$setProp(_el$124, "style", _v$39, _p$.e));
1919
+ _v$40 !== _p$.t && (_p$.t = _$setProp(_el$126, "fg", _v$40, _p$.t));
1920
+ _v$41 !== _p$.a && (_p$.a = _$setProp(_el$128, "style", _v$41, _p$.a));
1921
+ return _p$;
1922
+ }, {
1923
+ e: undefined,
1924
+ t: undefined,
1925
+ a: undefined
1926
+ });
1927
+ return _el$123;
1928
+ })()
1929
+ });
1930
+ }
1931
+ })];
1932
+ }
1933
+ }), null);
1934
+ _$effect((_$p) => _$setProp(_el$107, "fg", theme().text, _$p));
1935
+ return _el$104;
1936
+ }
1937
+ });
1938
+ }
1939
+ var id = "oc-forge";
1940
+ var tui = async (api) => {
1941
+ const tuiConfig = loadTuiConfig();
1942
+ const opts = {
1943
+ sidebar: tuiConfig?.sidebar ?? true,
1944
+ showLoops: tuiConfig?.showLoops ?? true,
1945
+ showVersion: tuiConfig?.showVersion ?? true
1946
+ };
1947
+ if (!opts.sidebar)
1948
+ return;
1949
+ api.command.register(() => {
1950
+ const directory = api.state.path.directory;
1951
+ const pid = resolveProjectId(directory);
1952
+ if (!pid)
1953
+ return [];
1954
+ const states = readLoopStates(pid);
1955
+ if (states.length === 0)
1956
+ return [];
1957
+ return [{
1958
+ title: "Forge: Show loops",
1959
+ value: "forge.loops.show",
1960
+ description: `${states.length} loop${states.length !== 1 ? "s" : ""}`,
1961
+ category: "Forge",
1962
+ onSelect: () => {
1963
+ const worktreeLoops = states.filter((l) => l.worktree);
1964
+ const loopOptions = worktreeLoops.map((l) => {
1965
+ const status = l.active ? l.phase : l.terminationReason?.replace(/_/g, " ") ?? "ended";
1966
+ return {
1967
+ title: l.name,
1968
+ value: l.name,
1969
+ description: status
1970
+ };
1971
+ });
1972
+ const showLoopList = () => {
1973
+ api.ui.dialog.setSize("large");
1974
+ api.ui.dialog.replace(() => _$createComponent(api.ui.DialogSelect, {
1975
+ title: "Loops",
1976
+ options: loopOptions,
1977
+ onSelect: (opt) => {
1978
+ const loopName = opt.value;
1979
+ const freshLoop = pid ? readLoopByName(pid, loopName) : null;
1980
+ if (freshLoop) {
1981
+ api.ui.dialog.setSize("medium");
1982
+ api.ui.dialog.replace(() => _$createComponent(LoopDetailsDialog, {
1983
+ api,
1984
+ loop: freshLoop,
1985
+ onBack: showLoopList,
1986
+ onRefresh: () => {}
1987
+ }));
1988
+ } else {
1989
+ api.ui.dialog.clear();
1990
+ }
1991
+ }
1992
+ }));
1993
+ };
1994
+ showLoopList();
1995
+ }
1996
+ }];
1997
+ });
1998
+ api.command.register(() => {
1999
+ const route = api.route.current;
2000
+ if (route.name !== "session")
2001
+ return [];
2002
+ const directory = api.state.path.directory;
2003
+ const pid = resolveProjectId(directory);
2004
+ if (!pid)
2005
+ return [];
2006
+ const sessionID = route.params?.sessionID;
2007
+ if (!sessionID)
2008
+ return [];
2009
+ const plan = readPlan(pid, sessionID);
2010
+ if (!plan)
2011
+ return [];
2012
+ return [{
2013
+ title: "Forge: View plan",
2014
+ value: "forge.plan.view",
2015
+ description: "View cached plan for this session",
2016
+ category: "Forge",
2017
+ onSelect: () => {
2018
+ const freshPlan = readPlan(pid, sessionID);
2019
+ if (!freshPlan) {
2020
+ api.ui.toast({
2021
+ message: "No plan found for this session",
2022
+ variant: "info",
2023
+ duration: 3000
2024
+ });
2025
+ return;
2026
+ }
2027
+ api.ui.dialog.setSize("large");
2028
+ api.ui.dialog.replace(() => _$createComponent(PlanViewerDialog, {
2029
+ api,
2030
+ planContent: freshPlan,
2031
+ projectId: pid,
2032
+ sessionId: sessionID
2033
+ }));
2034
+ }
2035
+ }];
2036
+ });
2037
+ api.slots.register({
2038
+ order: 150,
2039
+ slots: {
2040
+ sidebar_content(_ctx, slotProps) {
2041
+ return _$createComponent(Sidebar, {
2042
+ api,
2043
+ opts,
2044
+ get sessionId() {
2045
+ return slotProps.session_id;
2046
+ }
2047
+ });
2048
+ }
2049
+ }
2050
+ });
2051
+ };
2052
+ var plugin = {
2053
+ id,
2054
+ tui
2055
+ };
2056
+ var tui_default = plugin;
2057
+ export {
2058
+ readLoopStates,
2059
+ readLoopByName,
2060
+ tui_default as default
2061
+ };