projax 3.3.70 → 3.3.71

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 (39) hide show
  1. package/dist/agent-runner.d.ts +60 -0
  2. package/dist/agent-runner.js +382 -0
  3. package/dist/api/database.d.ts +38 -1
  4. package/dist/api/database.d.ts.map +1 -1
  5. package/dist/api/database.js +327 -0
  6. package/dist/api/database.js.map +1 -1
  7. package/dist/api/routes/agents.d.ts +4 -0
  8. package/dist/api/routes/agents.d.ts.map +1 -0
  9. package/dist/api/routes/agents.js +375 -0
  10. package/dist/api/routes/agents.js.map +1 -0
  11. package/dist/api/routes/index.d.ts.map +1 -1
  12. package/dist/api/routes/index.js +4 -0
  13. package/dist/api/routes/index.js.map +1 -1
  14. package/dist/api/routes/todos.d.ts +4 -0
  15. package/dist/api/routes/todos.d.ts.map +1 -0
  16. package/dist/api/routes/todos.js +595 -0
  17. package/dist/api/routes/todos.js.map +1 -0
  18. package/dist/api/types.d.ts +67 -0
  19. package/dist/api/types.d.ts.map +1 -1
  20. package/dist/core/database.d.ts +47 -0
  21. package/dist/core/database.js +48 -0
  22. package/dist/core-bridge.d.ts +1 -1
  23. package/dist/electron/core/database.d.ts +47 -0
  24. package/dist/electron/core/database.js +48 -0
  25. package/dist/electron/renderer/assets/index-6afBeDFD.js +66 -0
  26. package/dist/electron/renderer/assets/index-Bd3aFi7B.css +1 -0
  27. package/dist/electron/renderer/index.html +2 -2
  28. package/dist/index.js +6 -175
  29. package/dist/octopus-cli.d.ts +2 -0
  30. package/dist/octopus-cli.js +45 -0
  31. package/dist/octopus-show-tui.d.ts +1 -0
  32. package/dist/octopus-show-tui.js +802 -0
  33. package/dist/octopus-tui.d.ts +1 -0
  34. package/dist/octopus-tui.js +115 -0
  35. package/dist/prxi.js +373 -63
  36. package/dist/prxi.tsx +472 -65
  37. package/package.json +7 -2
  38. package/dist/electron/renderer/assets/index-Cd1mTO3s.js +0 -66
  39. package/dist/electron/renderer/assets/index-DJDPi0kp.css +0 -1
@@ -0,0 +1,802 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.launchOctopusShowTUI = launchOctopusShowTUI;
40
+ const react_1 = __importStar(require("react"));
41
+ const ink_1 = require("ink");
42
+ const ink_text_input_1 = __importDefault(require("ink-text-input"));
43
+ const path = __importStar(require("path"));
44
+ const os = __importStar(require("os"));
45
+ const fs = __importStar(require("fs"));
46
+ const child_process_1 = require("child_process");
47
+ // Color scheme
48
+ const colors = {
49
+ bgPrimary: '#0d1117',
50
+ bgSecondary: '#161b22',
51
+ borderColor: '#30363d',
52
+ textPrimary: '#c9d1d9',
53
+ textSecondary: '#8b949e',
54
+ textTertiary: '#6e7681',
55
+ accentCyan: '#39c5cf',
56
+ accentBlue: '#58a6ff',
57
+ accentGreen: '#3fb950',
58
+ accentPurple: '#bc8cff',
59
+ accentOrange: '#ffa657',
60
+ accentRed: '#f85149',
61
+ accentYellow: '#d29922',
62
+ };
63
+ // API helper
64
+ class ApiClient {
65
+ baseUrl;
66
+ constructor() {
67
+ const dataDir = path.join(os.homedir(), '.projax');
68
+ const portFile = path.join(dataDir, 'api-port.txt');
69
+ let port = 38124;
70
+ if (fs.existsSync(portFile)) {
71
+ try {
72
+ port = parseInt(fs.readFileSync(portFile, 'utf-8').trim(), 10) || 38124;
73
+ }
74
+ catch (e) { /* Ignore error, use default port */ }
75
+ }
76
+ this.baseUrl = `http://localhost:${port}/api`;
77
+ }
78
+ request(endpoint, options = {}) {
79
+ const url = `${this.baseUrl}${endpoint}`;
80
+ const method = options.method || 'GET';
81
+ const curlCmd = method === 'GET'
82
+ ? `curl -s -f "${url}"`
83
+ : method === 'DELETE'
84
+ ? `curl -s -f -X DELETE "${url}"`
85
+ : `curl -s -f -X ${method} -H "Content-Type: application/json" ${options.body ? `-d '${options.body}'` : ''} "${url}"`;
86
+ const result = (0, child_process_1.execSync)(curlCmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
87
+ if (!result || result.trim() === '') {
88
+ return undefined;
89
+ }
90
+ return JSON.parse(result);
91
+ }
92
+ // Projects
93
+ getProjects() {
94
+ return this.request('/projects') || [];
95
+ }
96
+ // Todo Lists
97
+ getTodoLists(projectId) {
98
+ return this.request(`/projects/${projectId}/todo-lists`) || [];
99
+ }
100
+ createTodoList(projectId, name, description) {
101
+ return this.request(`/projects/${projectId}/todo-lists`, {
102
+ method: 'POST',
103
+ body: JSON.stringify({ name, description }),
104
+ });
105
+ }
106
+ updateTodoList(id, updates) {
107
+ return this.request(`/todo-lists/${id}`, {
108
+ method: 'PUT',
109
+ body: JSON.stringify(updates),
110
+ });
111
+ }
112
+ deleteTodoList(id) {
113
+ this.request(`/todo-lists/${id}`, { method: 'DELETE' });
114
+ }
115
+ // Tasks
116
+ getTasks(listId) {
117
+ return this.request(`/todo-lists/${listId}/tasks`) || [];
118
+ }
119
+ getProjectTasks(projectId) {
120
+ return this.request(`/projects/${projectId}/tasks`) || [];
121
+ }
122
+ createTask(listId, title, priority, description) {
123
+ return this.request(`/todo-lists/${listId}/tasks`, {
124
+ method: 'POST',
125
+ body: JSON.stringify({ title, priority, description }),
126
+ });
127
+ }
128
+ updateTask(id, updates) {
129
+ return this.request(`/tasks/${id}`, {
130
+ method: 'PUT',
131
+ body: JSON.stringify(updates),
132
+ });
133
+ }
134
+ deleteTask(id) {
135
+ this.request(`/tasks/${id}`, { method: 'DELETE' });
136
+ }
137
+ toggleTask(id) {
138
+ return this.request(`/tasks/${id}/toggle`, { method: 'POST' });
139
+ }
140
+ // Agents
141
+ getAgents(projectId) {
142
+ return this.request(`/projects/${projectId}/agents`) || [];
143
+ }
144
+ assignAgent(taskId, agentId) {
145
+ return this.request(`/tasks/${taskId}/assign-agent`, {
146
+ method: 'POST',
147
+ body: JSON.stringify({ agent_id: agentId }),
148
+ });
149
+ }
150
+ startTask(taskId) {
151
+ return this.request(`/tasks/${taskId}/start`, { method: 'POST' });
152
+ }
153
+ mergeTask(taskId) {
154
+ return this.request(`/tasks/${taskId}/merge`, { method: 'POST' });
155
+ }
156
+ abortTask(taskId) {
157
+ return this.request(`/tasks/${taskId}/abort`, { method: 'POST' });
158
+ }
159
+ getRunningRuns() {
160
+ return this.request('/runs/running') || [];
161
+ }
162
+ }
163
+ const api = new ApiClient();
164
+ // Priority colors
165
+ const priorityColors = {
166
+ low: colors.textTertiary,
167
+ medium: colors.accentYellow,
168
+ high: colors.accentOrange,
169
+ urgent: colors.accentRed,
170
+ };
171
+ // Status colors and icons
172
+ const statusConfig = {
173
+ pending: { color: colors.textTertiary, icon: '○' },
174
+ in_progress: { color: colors.accentYellow, icon: '◐' },
175
+ completed: { color: colors.accentGreen, icon: '●' },
176
+ blocked: { color: colors.accentRed, icon: '✗' },
177
+ };
178
+ // Help Modal
179
+ const HelpModal = ({ onClose }) => {
180
+ (0, ink_1.useInput)((input, key) => {
181
+ if (key.escape || input === 'q' || key.return) {
182
+ onClose();
183
+ }
184
+ });
185
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentCyan, padding: 1, width: 80 },
186
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, "\uD83D\uDC19 Octopus - Task Manager Help"),
187
+ react_1.default.createElement(ink_1.Newline, null),
188
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Navigation:"),
189
+ react_1.default.createElement(ink_1.Text, null, " \u2191/k \u2193/j Move up/down in lists"),
190
+ react_1.default.createElement(ink_1.Text, null, " Tab/\u2190\u2192 Switch between panels"),
191
+ react_1.default.createElement(ink_1.Text, null, " Enter Select / Toggle task status"),
192
+ react_1.default.createElement(ink_1.Newline, null),
193
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Project Panel:"),
194
+ react_1.default.createElement(ink_1.Text, null, " n Create new todo list"),
195
+ react_1.default.createElement(ink_1.Text, null, " d Delete todo list"),
196
+ react_1.default.createElement(ink_1.Text, null, " r Rename todo list"),
197
+ react_1.default.createElement(ink_1.Newline, null),
198
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Task Panel:"),
199
+ react_1.default.createElement(ink_1.Text, null, " a Add new task"),
200
+ react_1.default.createElement(ink_1.Text, null, " e Edit task title"),
201
+ react_1.default.createElement(ink_1.Text, null, " d Delete task"),
202
+ react_1.default.createElement(ink_1.Text, null, " Space Toggle task status"),
203
+ react_1.default.createElement(ink_1.Text, null, " g Assign agent to task"),
204
+ react_1.default.createElement(ink_1.Text, null, " s Start agent (creates worktree)"),
205
+ react_1.default.createElement(ink_1.Text, null, " m Merge worktree back"),
206
+ react_1.default.createElement(ink_1.Text, null, " x Abort worktree"),
207
+ react_1.default.createElement(ink_1.Newline, null),
208
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "General:"),
209
+ react_1.default.createElement(ink_1.Text, null, " ? Show this help"),
210
+ react_1.default.createElement(ink_1.Text, null, " q/Esc Quit"),
211
+ react_1.default.createElement(ink_1.Newline, null),
212
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Press any key to close...")));
213
+ };
214
+ // Input Modal
215
+ const InputModal = ({ title, initialValue = '', onSubmit, onCancel }) => {
216
+ const [value, setValue] = (0, react_1.useState)(initialValue);
217
+ (0, ink_1.useInput)((input, key) => {
218
+ if (key.escape) {
219
+ onCancel();
220
+ }
221
+ if (key.return) {
222
+ if (value.trim()) {
223
+ onSubmit(value.trim());
224
+ }
225
+ }
226
+ });
227
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentCyan, padding: 1, width: 60 },
228
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, title),
229
+ react_1.default.createElement(ink_1.Newline, null),
230
+ react_1.default.createElement(ink_1.Box, null,
231
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " "),
232
+ react_1.default.createElement(ink_text_input_1.default, { value: value, onChange: setValue, placeholder: "Enter value..." })),
233
+ react_1.default.createElement(ink_1.Newline, null),
234
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Press Enter to confirm, Esc to cancel")));
235
+ };
236
+ // Confirm Modal
237
+ const ConfirmModal = ({ message, onConfirm, onCancel }) => {
238
+ (0, ink_1.useInput)((input, key) => {
239
+ if (key.escape || input === 'n') {
240
+ onCancel();
241
+ }
242
+ if (input === 'y' || key.return) {
243
+ onConfirm();
244
+ }
245
+ });
246
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentRed, padding: 1, width: 60 },
247
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentRed }, "Confirm Action"),
248
+ react_1.default.createElement(ink_1.Newline, null),
249
+ react_1.default.createElement(ink_1.Text, null, message),
250
+ react_1.default.createElement(ink_1.Newline, null),
251
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Press y/Enter to confirm, n/Esc to cancel")));
252
+ };
253
+ // Project List Panel
254
+ const ProjectListPanel = ({ projects, todoLists, selectedProjectIndex, selectedListIndex, isFocused, height, onSelectProject, onSelectList, }) => {
255
+ const scrollOffset = Math.max(0, selectedProjectIndex - Math.floor(height / 3));
256
+ const visibleProjects = projects.slice(scrollOffset, scrollOffset + height - 6);
257
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "30%", height: height, borderStyle: "round", borderColor: isFocused ? colors.accentCyan : colors.borderColor, padding: 1 },
258
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.textPrimary },
259
+ "Projects (",
260
+ projects.length,
261
+ ")"),
262
+ react_1.default.createElement(ink_1.Newline, null),
263
+ projects.length === 0 ? (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "No projects found")) : (react_1.default.createElement(ink_1.Box, { flexDirection: "column" }, visibleProjects.map((project, idx) => {
264
+ const actualIndex = scrollOffset + idx;
265
+ const isSelected = actualIndex === selectedProjectIndex;
266
+ const listCount = todoLists.filter(l => l.project_id === project.id).length;
267
+ return (react_1.default.createElement(ink_1.Box, { key: project.id },
268
+ react_1.default.createElement(ink_1.Text, { color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
269
+ isSelected ? '▶ ' : ' ',
270
+ project.name),
271
+ listCount > 0 && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
272
+ " [",
273
+ listCount,
274
+ "]"))));
275
+ }))),
276
+ todoLists.length > 0 && selectedProjectIndex < projects.length && (react_1.default.createElement(react_1.default.Fragment, null,
277
+ react_1.default.createElement(ink_1.Newline, null),
278
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentPurple }, "Todo Lists"),
279
+ todoLists
280
+ .filter(l => l.project_id === projects[selectedProjectIndex]?.id)
281
+ .map((list, idx) => {
282
+ const isSelected = idx === selectedListIndex;
283
+ return (react_1.default.createElement(ink_1.Box, { key: list.id },
284
+ react_1.default.createElement(ink_1.Text, { color: isSelected ? colors.accentPurple : colors.textSecondary, bold: isSelected },
285
+ isSelected ? '▶ ' : ' ',
286
+ list.name),
287
+ list.task_count !== undefined && (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
288
+ ' ',
289
+ "[",
290
+ list.completed_count || 0,
291
+ "/",
292
+ list.task_count,
293
+ "]"))));
294
+ })))));
295
+ };
296
+ // Task Panel
297
+ const TaskPanel = ({ tasks, agents, runningRuns, selectedTaskIndex, selectedList, isFocused, height, onSelectTask, }) => {
298
+ const scrollOffset = Math.max(0, selectedTaskIndex - Math.floor(height / 3));
299
+ const visibleTasks = tasks.slice(scrollOffset, scrollOffset + height - 10);
300
+ const getAgentName = (agentId) => {
301
+ if (!agentId)
302
+ return null;
303
+ const agent = agents.find(a => a.id === agentId);
304
+ return agent?.name || `Agent ${agentId}`;
305
+ };
306
+ const isTaskRunning = (taskId) => {
307
+ return runningRuns.some(r => r.task_id === taskId && r.status === 'running');
308
+ };
309
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", width: "70%", height: height, borderStyle: "round", borderColor: isFocused ? colors.accentCyan : colors.borderColor, padding: 1 },
310
+ react_1.default.createElement(ink_1.Box, { justifyContent: "space-between" },
311
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.textPrimary }, selectedList?.name || 'Tasks'),
312
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary },
313
+ tasks.filter(t => t.status === 'completed').length,
314
+ "/",
315
+ tasks.length,
316
+ " completed")),
317
+ react_1.default.createElement(ink_1.Newline, null),
318
+ tasks.length === 0 ? (react_1.default.createElement(ink_1.Text, { color: colors.textTertiary }, "No tasks. Press 'a' to add a new task.")) : (react_1.default.createElement(ink_1.Box, { flexDirection: "column" }, visibleTasks.map((task, idx) => {
319
+ const actualIndex = scrollOffset + idx;
320
+ const isSelected = actualIndex === selectedTaskIndex;
321
+ const config = statusConfig[task.status];
322
+ const isRunning = isTaskRunning(task.id);
323
+ const agentName = getAgentName(task.assignee_agent_id);
324
+ return (react_1.default.createElement(ink_1.Box, { key: task.id, flexDirection: "column" },
325
+ react_1.default.createElement(ink_1.Box, null,
326
+ react_1.default.createElement(ink_1.Text, { color: isSelected ? colors.accentCyan : colors.textPrimary, bold: isSelected },
327
+ isSelected ? '▶ ' : ' ',
328
+ react_1.default.createElement(ink_1.Text, { color: config.color }, config.icon),
329
+ ' ',
330
+ task.title),
331
+ isRunning && (react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, " \u25CF"))),
332
+ react_1.default.createElement(ink_1.Box, { marginLeft: 4 },
333
+ react_1.default.createElement(ink_1.Text, { color: priorityColors[task.priority] },
334
+ "[",
335
+ task.priority,
336
+ "]"),
337
+ agentName && (react_1.default.createElement(ink_1.Text, { color: colors.accentPurple },
338
+ " @",
339
+ agentName)),
340
+ task.worktree_path && (react_1.default.createElement(ink_1.Text, { color: colors.accentBlue }, " \uD83C\uDF3F")))));
341
+ }))),
342
+ runningRuns.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
343
+ react_1.default.createElement(ink_1.Newline, null),
344
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentGreen },
345
+ "Running Agents (",
346
+ runningRuns.length,
347
+ ")"),
348
+ runningRuns.map(run => {
349
+ const task = tasks.find(t => t.id === run.task_id);
350
+ const agent = agents.find(a => a.id === run.agent_id);
351
+ const elapsed = Math.floor((Date.now() - run.started_at * 1000) / 1000);
352
+ const minutes = Math.floor(elapsed / 60);
353
+ const seconds = elapsed % 60;
354
+ return (react_1.default.createElement(ink_1.Box, { key: run.id },
355
+ react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF "),
356
+ react_1.default.createElement(ink_1.Text, { color: colors.textPrimary }, agent?.name || `Agent ${run.agent_id}`),
357
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary },
358
+ ' → ',
359
+ task?.title || `Task ${run.task_id}`),
360
+ react_1.default.createElement(ink_1.Text, { color: colors.textTertiary },
361
+ ' ',
362
+ "(",
363
+ minutes,
364
+ "m ",
365
+ seconds,
366
+ "s)")));
367
+ })))));
368
+ };
369
+ // Status Bar
370
+ const StatusBar = ({ focusedPanel, selectedTask }) => {
371
+ if (focusedPanel === 'projects') {
372
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column" },
373
+ react_1.default.createElement(ink_1.Box, null,
374
+ react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF API"),
375
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " | "),
376
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Focus: Projects")),
377
+ react_1.default.createElement(ink_1.Box, null,
378
+ react_1.default.createElement(ink_1.Text, { bold: true }, "\u2191\u2193/jk"),
379
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Navigate | "),
380
+ react_1.default.createElement(ink_1.Text, { bold: true }, "n"),
381
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " New list | "),
382
+ react_1.default.createElement(ink_1.Text, { bold: true }, "d"),
383
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Delete | "),
384
+ react_1.default.createElement(ink_1.Text, { bold: true }, "Tab"),
385
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Switch | "),
386
+ react_1.default.createElement(ink_1.Text, { bold: true }, "?"),
387
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Help | "),
388
+ react_1.default.createElement(ink_1.Text, { bold: true }, "q"),
389
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Quit"))));
390
+ }
391
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column" },
392
+ react_1.default.createElement(ink_1.Box, null,
393
+ react_1.default.createElement(ink_1.Text, { color: colors.accentGreen }, "\u25CF API"),
394
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " | "),
395
+ react_1.default.createElement(ink_1.Text, { color: colors.accentCyan }, "Focus: Tasks"),
396
+ selectedTask && (react_1.default.createElement(react_1.default.Fragment, null,
397
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " | "),
398
+ react_1.default.createElement(ink_1.Text, { color: colors.textPrimary }, selectedTask.title)))),
399
+ react_1.default.createElement(ink_1.Box, null,
400
+ react_1.default.createElement(ink_1.Text, { bold: true }, "\u2191\u2193/jk"),
401
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Navigate | "),
402
+ react_1.default.createElement(ink_1.Text, { bold: true }, "Space"),
403
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Toggle | "),
404
+ react_1.default.createElement(ink_1.Text, { bold: true }, "a"),
405
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Add | "),
406
+ react_1.default.createElement(ink_1.Text, { bold: true }, "s"),
407
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Start | "),
408
+ react_1.default.createElement(ink_1.Text, { bold: true }, "m"),
409
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Merge | "),
410
+ react_1.default.createElement(ink_1.Text, { bold: true }, "g"),
411
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Assign | "),
412
+ react_1.default.createElement(ink_1.Text, { bold: true }, "Tab"),
413
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Switch | "),
414
+ react_1.default.createElement(ink_1.Text, { bold: true }, "?"),
415
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " Help"))));
416
+ };
417
+ // Main App
418
+ const App = () => {
419
+ const { exit } = (0, ink_1.useApp)();
420
+ // Data state
421
+ const [projects, setProjects] = (0, react_1.useState)([]);
422
+ const [todoLists, setTodoLists] = (0, react_1.useState)([]);
423
+ const [tasks, setTasks] = (0, react_1.useState)([]);
424
+ const [agents, setAgents] = (0, react_1.useState)([]);
425
+ const [runningRuns, setRunningRuns] = (0, react_1.useState)([]);
426
+ // UI state
427
+ const [focusedPanel, setFocusedPanel] = (0, react_1.useState)('projects');
428
+ const [selectedProjectIndex, setSelectedProjectIndex] = (0, react_1.useState)(0);
429
+ const [selectedListIndex, setSelectedListIndex] = (0, react_1.useState)(0);
430
+ const [selectedTaskIndex, setSelectedTaskIndex] = (0, react_1.useState)(0);
431
+ // Modal state
432
+ const [showHelp, setShowHelp] = (0, react_1.useState)(false);
433
+ const [showInput, setShowInput] = (0, react_1.useState)(null);
434
+ const [showConfirm, setShowConfirm] = (0, react_1.useState)(null);
435
+ const [error, setError] = (0, react_1.useState)(null);
436
+ const terminalHeight = process.stdout.rows || 24;
437
+ const panelHeight = terminalHeight - 6;
438
+ // Load initial data
439
+ (0, react_1.useEffect)(() => {
440
+ loadData();
441
+ const interval = setInterval(loadRunningRuns, 5000);
442
+ return () => clearInterval(interval);
443
+ }, []);
444
+ // Load tasks when list selection changes
445
+ (0, react_1.useEffect)(() => {
446
+ loadTasks();
447
+ loadAgents();
448
+ }, [selectedProjectIndex, selectedListIndex, todoLists]);
449
+ const loadData = (0, react_1.useCallback)(() => {
450
+ try {
451
+ const projs = api.getProjects();
452
+ setProjects(projs);
453
+ // Load all todo lists
454
+ const allLists = [];
455
+ projs.forEach(p => {
456
+ const lists = api.getTodoLists(p.id);
457
+ allLists.push(...lists);
458
+ });
459
+ setTodoLists(allLists);
460
+ }
461
+ catch (err) {
462
+ setError(err instanceof Error ? err.message : 'Failed to load data');
463
+ }
464
+ }, []);
465
+ const loadTasks = (0, react_1.useCallback)(() => {
466
+ if (projects.length === 0)
467
+ return;
468
+ const project = projects[selectedProjectIndex];
469
+ if (!project)
470
+ return;
471
+ const projectLists = todoLists.filter(l => l.project_id === project.id);
472
+ const selectedList = projectLists[selectedListIndex];
473
+ if (selectedList) {
474
+ try {
475
+ const taskList = api.getTasks(selectedList.id);
476
+ setTasks(taskList);
477
+ }
478
+ catch (err) {
479
+ setTasks([]);
480
+ }
481
+ }
482
+ else {
483
+ // Show all tasks for project
484
+ try {
485
+ const allTasks = api.getProjectTasks(project.id);
486
+ setTasks(allTasks);
487
+ }
488
+ catch (err) {
489
+ setTasks([]);
490
+ }
491
+ }
492
+ setSelectedTaskIndex(0);
493
+ }, [projects, selectedProjectIndex, selectedListIndex, todoLists]);
494
+ const loadAgents = (0, react_1.useCallback)(() => {
495
+ if (projects.length === 0)
496
+ return;
497
+ const project = projects[selectedProjectIndex];
498
+ if (!project)
499
+ return;
500
+ try {
501
+ const agentList = api.getAgents(project.id);
502
+ setAgents(agentList);
503
+ }
504
+ catch (err) {
505
+ setAgents([]);
506
+ }
507
+ }, [projects, selectedProjectIndex]);
508
+ const loadRunningRuns = (0, react_1.useCallback)(() => {
509
+ try {
510
+ const runs = api.getRunningRuns();
511
+ setRunningRuns(runs);
512
+ }
513
+ catch {
514
+ setRunningRuns([]);
515
+ }
516
+ }, []);
517
+ // Get selected items
518
+ const selectedProject = projects[selectedProjectIndex];
519
+ const projectLists = todoLists.filter(l => l.project_id === selectedProject?.id);
520
+ const selectedList = projectLists[selectedListIndex] || null;
521
+ const selectedTask = tasks[selectedTaskIndex] || null;
522
+ // Input handling
523
+ (0, ink_1.useInput)((input, key) => {
524
+ // Handle modals
525
+ if (showHelp) {
526
+ if (key.escape || input === 'q' || key.return) {
527
+ setShowHelp(false);
528
+ }
529
+ return;
530
+ }
531
+ if (showInput) {
532
+ return; // Input modal handles its own input
533
+ }
534
+ if (showConfirm) {
535
+ return; // Confirm modal handles its own input
536
+ }
537
+ if (error) {
538
+ if (key.escape || key.return) {
539
+ setError(null);
540
+ }
541
+ return;
542
+ }
543
+ // Quit
544
+ if (input === 'q' || key.escape) {
545
+ exit();
546
+ return;
547
+ }
548
+ // Help
549
+ if (input === '?') {
550
+ setShowHelp(true);
551
+ return;
552
+ }
553
+ // Switch panels
554
+ if (key.tab || key.leftArrow || key.rightArrow) {
555
+ setFocusedPanel(prev => prev === 'projects' ? 'tasks' : 'projects');
556
+ return;
557
+ }
558
+ // Project panel navigation
559
+ if (focusedPanel === 'projects') {
560
+ if (key.upArrow || input === 'k') {
561
+ if (key.shift || selectedListIndex === 0) {
562
+ setSelectedProjectIndex(prev => Math.max(0, prev - 1));
563
+ setSelectedListIndex(0);
564
+ }
565
+ else {
566
+ setSelectedListIndex(prev => Math.max(0, prev - 1));
567
+ }
568
+ return;
569
+ }
570
+ if (key.downArrow || input === 'j') {
571
+ const projectListsCount = todoLists.filter(l => l.project_id === selectedProject?.id).length;
572
+ if (key.shift || selectedListIndex >= projectListsCount - 1) {
573
+ setSelectedProjectIndex(prev => Math.min(projects.length - 1, prev + 1));
574
+ setSelectedListIndex(0);
575
+ }
576
+ else {
577
+ setSelectedListIndex(prev => Math.min(projectListsCount - 1, prev + 1));
578
+ }
579
+ return;
580
+ }
581
+ // New list
582
+ if (input === 'n' && selectedProject) {
583
+ setShowInput({
584
+ type: 'newList',
585
+ title: `New Todo List for ${selectedProject.name}`,
586
+ });
587
+ return;
588
+ }
589
+ // Delete list
590
+ if (input === 'd' && selectedList) {
591
+ setShowConfirm({
592
+ message: `Delete "${selectedList.name}" and all its tasks?`,
593
+ action: () => {
594
+ try {
595
+ api.deleteTodoList(selectedList.id);
596
+ loadData();
597
+ setSelectedListIndex(0);
598
+ }
599
+ catch (err) {
600
+ setError(err instanceof Error ? err.message : 'Failed to delete list');
601
+ }
602
+ },
603
+ });
604
+ return;
605
+ }
606
+ // Rename list
607
+ if (input === 'r' && selectedList) {
608
+ setShowInput({
609
+ type: 'renameList',
610
+ title: 'Rename Todo List',
611
+ initialValue: selectedList.name,
612
+ });
613
+ return;
614
+ }
615
+ }
616
+ // Task panel navigation
617
+ if (focusedPanel === 'tasks') {
618
+ if (key.upArrow || input === 'k') {
619
+ setSelectedTaskIndex(prev => Math.max(0, prev - 1));
620
+ return;
621
+ }
622
+ if (key.downArrow || input === 'j') {
623
+ setSelectedTaskIndex(prev => Math.min(tasks.length - 1, prev + 1));
624
+ return;
625
+ }
626
+ // Toggle task status
627
+ if (key.return || input === ' ') {
628
+ if (selectedTask) {
629
+ try {
630
+ api.toggleTask(selectedTask.id);
631
+ loadTasks();
632
+ loadRunningRuns();
633
+ }
634
+ catch (err) {
635
+ setError(err instanceof Error ? err.message : 'Failed to toggle task');
636
+ }
637
+ }
638
+ return;
639
+ }
640
+ // Add task
641
+ if (input === 'a' && selectedList) {
642
+ setShowInput({
643
+ type: 'newTask',
644
+ title: `New Task in ${selectedList.name}`,
645
+ });
646
+ return;
647
+ }
648
+ // Edit task
649
+ if (input === 'e' && selectedTask) {
650
+ setShowInput({
651
+ type: 'editTask',
652
+ title: 'Edit Task',
653
+ initialValue: selectedTask.title,
654
+ });
655
+ return;
656
+ }
657
+ // Delete task
658
+ if (input === 'd' && selectedTask) {
659
+ setShowConfirm({
660
+ message: `Delete task "${selectedTask.title}"?`,
661
+ action: () => {
662
+ try {
663
+ api.deleteTask(selectedTask.id);
664
+ loadTasks();
665
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
666
+ }
667
+ catch (err) {
668
+ setError(err instanceof Error ? err.message : 'Failed to delete task');
669
+ }
670
+ },
671
+ });
672
+ return;
673
+ }
674
+ // Assign agent
675
+ if (input === 'g' && selectedTask && agents.length > 0) {
676
+ // Cycle through agents
677
+ const currentIdx = agents.findIndex(a => a.id === selectedTask.assignee_agent_id);
678
+ const nextIdx = (currentIdx + 1) % (agents.length + 1);
679
+ const nextAgentId = nextIdx === agents.length ? null : agents[nextIdx].id;
680
+ try {
681
+ api.assignAgent(selectedTask.id, nextAgentId);
682
+ loadTasks();
683
+ }
684
+ catch (err) {
685
+ setError(err instanceof Error ? err.message : 'Failed to assign agent');
686
+ }
687
+ return;
688
+ }
689
+ // Start agent on task
690
+ if (input === 's' && selectedTask && selectedTask.assignee_agent_id) {
691
+ try {
692
+ api.startTask(selectedTask.id);
693
+ loadTasks();
694
+ loadRunningRuns();
695
+ }
696
+ catch (err) {
697
+ setError(err instanceof Error ? err.message : 'Failed to start task');
698
+ }
699
+ return;
700
+ }
701
+ // Merge task worktree
702
+ if (input === 'm' && selectedTask && selectedTask.worktree_path) {
703
+ setShowConfirm({
704
+ message: `Merge worktree for "${selectedTask.title}"?`,
705
+ action: () => {
706
+ try {
707
+ api.mergeTask(selectedTask.id);
708
+ loadTasks();
709
+ loadRunningRuns();
710
+ }
711
+ catch (err) {
712
+ setError(err instanceof Error ? err.message : 'Failed to merge task');
713
+ }
714
+ },
715
+ });
716
+ return;
717
+ }
718
+ // Abort task worktree
719
+ if (input === 'x' && selectedTask && selectedTask.worktree_path) {
720
+ setShowConfirm({
721
+ message: `Abort worktree for "${selectedTask.title}"? Changes will be lost.`,
722
+ action: () => {
723
+ try {
724
+ api.abortTask(selectedTask.id);
725
+ loadTasks();
726
+ loadRunningRuns();
727
+ }
728
+ catch (err) {
729
+ setError(err instanceof Error ? err.message : 'Failed to abort task');
730
+ }
731
+ },
732
+ });
733
+ return;
734
+ }
735
+ }
736
+ });
737
+ // Handle input modal submit
738
+ const handleInputSubmit = (value) => {
739
+ if (!showInput)
740
+ return;
741
+ try {
742
+ if (showInput.type === 'newList' && selectedProject) {
743
+ api.createTodoList(selectedProject.id, value);
744
+ loadData();
745
+ }
746
+ else if (showInput.type === 'renameList' && selectedList) {
747
+ api.updateTodoList(selectedList.id, { name: value });
748
+ loadData();
749
+ }
750
+ else if (showInput.type === 'newTask' && selectedList) {
751
+ api.createTask(selectedList.id, value);
752
+ loadTasks();
753
+ }
754
+ else if (showInput.type === 'editTask' && selectedTask) {
755
+ api.updateTask(selectedTask.id, { title: value });
756
+ loadTasks();
757
+ }
758
+ }
759
+ catch (err) {
760
+ setError(err instanceof Error ? err.message : 'Operation failed');
761
+ }
762
+ setShowInput(null);
763
+ };
764
+ // Render modals
765
+ if (showHelp) {
766
+ return (react_1.default.createElement(ink_1.Box, { padding: 1 },
767
+ react_1.default.createElement(HelpModal, { onClose: () => setShowHelp(false) })));
768
+ }
769
+ if (showInput) {
770
+ return (react_1.default.createElement(ink_1.Box, { padding: 1 },
771
+ react_1.default.createElement(InputModal, { title: showInput.title, initialValue: showInput.initialValue, onSubmit: handleInputSubmit, onCancel: () => setShowInput(null) })));
772
+ }
773
+ if (showConfirm) {
774
+ return (react_1.default.createElement(ink_1.Box, { padding: 1 },
775
+ react_1.default.createElement(ConfirmModal, { message: showConfirm.message, onConfirm: () => {
776
+ showConfirm.action();
777
+ setShowConfirm(null);
778
+ }, onCancel: () => setShowConfirm(null) })));
779
+ }
780
+ if (error) {
781
+ return (react_1.default.createElement(ink_1.Box, { padding: 1 },
782
+ react_1.default.createElement(ink_1.Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.accentRed, padding: 1, width: 60 },
783
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentRed }, "Error"),
784
+ react_1.default.createElement(ink_1.Newline, null),
785
+ react_1.default.createElement(ink_1.Text, null, error),
786
+ react_1.default.createElement(ink_1.Newline, null),
787
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, "Press any key to dismiss..."))));
788
+ }
789
+ return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", height: terminalHeight },
790
+ react_1.default.createElement(ink_1.Box, { paddingX: 1 },
791
+ react_1.default.createElement(ink_1.Text, { bold: true, color: colors.accentCyan }, "\uD83D\uDC19 Octopus Task Manager"),
792
+ react_1.default.createElement(ink_1.Text, { color: colors.textSecondary }, " - Manage project tasks with AI agents")),
793
+ react_1.default.createElement(ink_1.Box, { flexDirection: "row", height: panelHeight, flexGrow: 0, flexShrink: 0 },
794
+ react_1.default.createElement(ProjectListPanel, { projects: projects, todoLists: todoLists, selectedProjectIndex: selectedProjectIndex, selectedListIndex: selectedListIndex, isFocused: focusedPanel === 'projects', height: panelHeight, onSelectProject: setSelectedProjectIndex, onSelectList: setSelectedListIndex }),
795
+ react_1.default.createElement(ink_1.Box, { width: 1 }),
796
+ react_1.default.createElement(TaskPanel, { tasks: tasks, agents: agents, runningRuns: runningRuns, selectedTaskIndex: selectedTaskIndex, selectedList: selectedList, isFocused: focusedPanel === 'tasks', height: panelHeight, onSelectTask: setSelectedTaskIndex })),
797
+ react_1.default.createElement(ink_1.Box, { paddingX: 1, borderStyle: "single", borderColor: colors.borderColor, flexShrink: 0, height: 4 },
798
+ react_1.default.createElement(StatusBar, { focusedPanel: focusedPanel, selectedTask: selectedTask }))));
799
+ };
800
+ function launchOctopusShowTUI() {
801
+ (0, ink_1.render)(react_1.default.createElement(App, null));
802
+ }