claude-dashboard 0.2.0 → 0.4.0

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 (90) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +36 -13
  3. package/.next/app-path-routes-manifest.json +3 -0
  4. package/.next/build-manifest.json +2 -2
  5. package/.next/next-server.js.nft.json +1 -1
  6. package/.next/required-server-files.json +3 -3
  7. package/.next/routes-manifest.json +8 -0
  8. package/.next/server/app/_not-found/page.js +2 -2
  9. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  10. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +15 -14
  13. package/.next/server/app/api/health/route.js.nft.json +1 -1
  14. package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  15. package/.next/server/app/api/usage/route.js.nft.json +1 -1
  16. package/.next/server/app/api/usage/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/workflows/[id]/route.js +1 -13
  18. package/.next/server/app/api/workflows/[id]/route.js.nft.json +1 -1
  19. package/.next/server/app/api/workflows/[id]/route_client-reference-manifest.js +1 -1
  20. package/.next/server/app/api/workflows/cleanup/route.js +1 -0
  21. package/.next/server/app/api/workflows/cleanup/route.js.nft.json +1 -0
  22. package/.next/server/app/api/workflows/cleanup/route_client-reference-manifest.js +1 -0
  23. package/.next/server/app/api/workflows/metrics/route.js +1 -0
  24. package/.next/server/app/api/workflows/metrics/route.js.nft.json +1 -0
  25. package/.next/server/app/api/workflows/metrics/route_client-reference-manifest.js +1 -0
  26. package/.next/server/app/api/workflows/route.js +1 -13
  27. package/.next/server/app/api/workflows/route.js.nft.json +1 -1
  28. package/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/history/[id]/page.js +5 -0
  30. package/.next/server/app/history/[id]/page.js.nft.json +1 -0
  31. package/.next/server/app/history/[id]/page_client-reference-manifest.js +1 -0
  32. package/.next/server/app/history/page.js +1 -1
  33. package/.next/server/app/history/page.js.nft.json +1 -1
  34. package/.next/server/app/history/page_client-reference-manifest.js +1 -1
  35. package/.next/server/app/history.html +1 -1
  36. package/.next/server/app/history.rsc +19 -18
  37. package/.next/server/app/index.html +1 -1
  38. package/.next/server/app/index.rsc +19 -18
  39. package/.next/server/app/page.js +3 -3
  40. package/.next/server/app/page.js.nft.json +1 -1
  41. package/.next/server/app/page_client-reference-manifest.js +1 -1
  42. package/.next/server/app-paths-manifest.json +3 -0
  43. package/.next/server/chunks/{737.js → 205.js} +1 -1
  44. package/.next/server/chunks/53.js +63 -0
  45. package/.next/server/chunks/859.js +1 -0
  46. package/.next/server/chunks/913.js +1 -0
  47. package/.next/server/pages/404.html +1 -1
  48. package/.next/server/pages/500.html +1 -1
  49. package/.next/server/pages/_app.js.nft.json +1 -1
  50. package/.next/server/pages/_document.js.nft.json +1 -1
  51. package/.next/static/BXp78QLX_5XRObTh2VELw/_buildManifest.js +1 -0
  52. package/.next/static/chunks/145-10d5b3969d67e4c0.js +1 -0
  53. package/.next/static/chunks/203-b5ea437c1564ad7d.js +1 -0
  54. package/.next/static/chunks/{825-4978b2439a33cef8.js → 379-4e24b4cec3288453.js} +2 -2
  55. package/.next/static/chunks/app/api/health/route-903cc84446e35d73.js +1 -0
  56. package/.next/static/chunks/app/api/usage/route-903cc84446e35d73.js +1 -0
  57. package/.next/static/chunks/app/api/workflows/[id]/route-903cc84446e35d73.js +1 -0
  58. package/.next/static/chunks/app/api/workflows/cleanup/route-903cc84446e35d73.js +1 -0
  59. package/.next/static/chunks/app/api/workflows/metrics/route-903cc84446e35d73.js +1 -0
  60. package/.next/static/chunks/app/api/workflows/route-903cc84446e35d73.js +1 -0
  61. package/.next/static/chunks/app/history/[id]/page-91f0f7e7565dea12.js +1 -0
  62. package/.next/static/chunks/app/history/page-3942390e1d606f32.js +1 -0
  63. package/.next/static/chunks/app/layout-71710c37c90ec543.js +1 -0
  64. package/.next/static/chunks/app/page-1a412f0d8f879068.js +1 -0
  65. package/.next/static/css/{75908259b9e378e4.css → 844cb206278a3d3e.css} +1 -1
  66. package/README.md +31 -0
  67. package/README.zh-TW.md +31 -0
  68. package/dist/src/lib/db/connection.js +22 -5
  69. package/dist/src/lib/db/queries.js +111 -12
  70. package/dist/src/lib/i18n/messages.js +238 -0
  71. package/dist/src/lib/i18n/useI18n.js +20 -0
  72. package/dist/src/lib/terminal/pty-manager.js +15 -2
  73. package/dist/src/lib/websocket/server.js +40 -4
  74. package/dist/src/lib/workflow/engine.js +15 -9
  75. package/dist/src/lib/workflow/pipeline.js +4 -3
  76. package/dist/src/lib/workflow/types.js +13 -0
  77. package/package.json +3 -6
  78. package/.next/server/chunks/715.js +0 -1
  79. package/.next/server/chunks/849.js +0 -1
  80. package/.next/static/4fPtjKSL5y0HNNsoLs4PS/_buildManifest.js +0 -1
  81. package/.next/static/chunks/145-3c417ba81c4b45f5.js +0 -1
  82. package/.next/static/chunks/619-7492253b494230b1.js +0 -1
  83. package/.next/static/chunks/app/api/health/route-36fb14cc6a282394.js +0 -1
  84. package/.next/static/chunks/app/api/usage/route-36fb14cc6a282394.js +0 -1
  85. package/.next/static/chunks/app/api/workflows/[id]/route-36fb14cc6a282394.js +0 -1
  86. package/.next/static/chunks/app/api/workflows/route-36fb14cc6a282394.js +0 -1
  87. package/.next/static/chunks/app/history/page-eef8489b051348cb.js +0 -1
  88. package/.next/static/chunks/app/layout-37b1ff7dda2e8465.js +0 -1
  89. package/.next/static/chunks/app/page-dd53f4f52bd5e72d.js +0 -1
  90. /package/.next/static/{4fPtjKSL5y0HNNsoLs4PS → BXp78QLX_5XRObTh2VELw}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
2
  import { getDb } from "./connection.js";
3
- import { AGENT_ORDER, } from "../workflow/types.js";
3
+ import { AGENT_ORDER, normalizeExecutionPlan, } from "../workflow/types.js";
4
4
  function rowToWorkflow(row) {
5
5
  return {
6
6
  id: row.id,
@@ -35,11 +35,12 @@ function rowToStep(row) {
35
35
  // Workflow CRUD
36
36
  // ---------------------------------------------------------------------------
37
37
  /**
38
- * Create a new workflow together with its five agent-step rows (one per role
39
- * in `AGENT_ORDER`). Everything runs inside a single transaction so that the
40
- * workflow and its steps are always created atomically.
38
+ * Create a new workflow together with one agent-step row per role in
39
+ * `AGENT_ORDER`. Steps outside the requested execution plan are marked as
40
+ * `skipped`. Everything runs inside a single transaction so workflow + steps
41
+ * are always created atomically.
41
42
  */
42
- export function createWorkflow(id, title, userPrompt, projectPath) {
43
+ export function createWorkflow(id, title, userPrompt, projectPath, executionPlan) {
43
44
  const db = getDb();
44
45
  const insertWorkflow = db.prepare(`
45
46
  INSERT INTO workflows (id, title, user_prompt, status, current_step_index, project_path)
@@ -47,8 +48,9 @@ export function createWorkflow(id, title, userPrompt, projectPath) {
47
48
  `);
48
49
  const insertStep = db.prepare(`
49
50
  INSERT INTO agent_steps (id, workflow_id, role, status)
50
- VALUES (@id, @workflowId, @role, 'pending')
51
+ VALUES (@id, @workflowId, @role, @status)
51
52
  `);
53
+ const selected = new Set(normalizeExecutionPlan(executionPlan));
52
54
  const txn = db.transaction(() => {
53
55
  insertWorkflow.run({ id, title, userPrompt, projectPath });
54
56
  for (const role of AGENT_ORDER) {
@@ -56,6 +58,7 @@ export function createWorkflow(id, title, userPrompt, projectPath) {
56
58
  id: uuidv4(),
57
59
  workflowId: id,
58
60
  role,
61
+ status: selected.has(role) ? 'pending' : 'skipped',
59
62
  });
60
63
  }
61
64
  });
@@ -71,15 +74,44 @@ export function getWorkflow(id) {
71
74
  const row = stmt.get(id);
72
75
  return row ? rowToWorkflow(row) : null;
73
76
  }
74
- /**
75
- * List workflows ordered by creation time (newest first).
76
- */
77
- export function listWorkflows(limit = 50, offset = 0) {
77
+ export function listWorkflows(limit = 50, offset = 0, filters) {
78
78
  const db = getDb();
79
- const stmt = db.prepare('SELECT * FROM workflows ORDER BY created_at DESC LIMIT ? OFFSET ?');
80
- const rows = stmt.all(limit, offset);
79
+ const where = [];
80
+ const params = {
81
+ limit,
82
+ offset,
83
+ };
84
+ if (filters === null || filters === void 0 ? void 0 : filters.status) {
85
+ where.push('status = @status');
86
+ params.status = filters.status;
87
+ }
88
+ if ((filters === null || filters === void 0 ? void 0 : filters.q) && filters.q.trim()) {
89
+ where.push('(title LIKE @q OR user_prompt LIKE @q)');
90
+ params.q = `%${filters.q.trim()}%`;
91
+ }
92
+ const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
93
+ const stmt = db.prepare(`SELECT * FROM workflows ${whereSql} ORDER BY created_at DESC LIMIT @limit OFFSET @offset`);
94
+ const rows = stmt.all(params);
81
95
  return rows.map(rowToWorkflow);
82
96
  }
97
+ export function countWorkflows(filters) {
98
+ var _a;
99
+ const db = getDb();
100
+ const where = [];
101
+ const params = {};
102
+ if (filters === null || filters === void 0 ? void 0 : filters.status) {
103
+ where.push('status = @status');
104
+ params.status = filters.status;
105
+ }
106
+ if ((filters === null || filters === void 0 ? void 0 : filters.q) && filters.q.trim()) {
107
+ where.push('(title LIKE @q OR user_prompt LIKE @q)');
108
+ params.q = `%${filters.q.trim()}%`;
109
+ }
110
+ const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
111
+ const stmt = db.prepare(`SELECT COUNT(*) as count FROM workflows ${whereSql}`);
112
+ const row = (Object.keys(params).length > 0 ? stmt.get(params) : stmt.get());
113
+ return (_a = row === null || row === void 0 ? void 0 : row.count) !== null && _a !== void 0 ? _a : 0;
114
+ }
83
115
  /**
84
116
  * Update a workflow's status (and optionally the current step index).
85
117
  *
@@ -167,3 +199,70 @@ export function updateStepStatus(id, updates) {
167
199
  const sql = `UPDATE agent_steps SET ${setClauses.join(', ')} WHERE id = @id`;
168
200
  db.prepare(sql).run(params);
169
201
  }
202
+ export function cleanupWorkflows(policy) {
203
+ var _a, _b;
204
+ const db = getDb();
205
+ const keepDays = (_a = policy.keepDays) !== null && _a !== void 0 ? _a : 0;
206
+ const keepLatest = (_b = policy.keepLatest) !== null && _b !== void 0 ? _b : 0;
207
+ const idsToDelete = new Set();
208
+ if (keepDays > 0) {
209
+ const oldRows = db
210
+ .prepare(`SELECT id FROM workflows WHERE datetime(created_at) < datetime('now', @cutoff)`)
211
+ .all({ cutoff: `-${keepDays} days` });
212
+ for (const row of oldRows)
213
+ idsToDelete.add(row.id);
214
+ }
215
+ if (keepLatest > 0) {
216
+ const overflowRows = db
217
+ .prepare(`SELECT id FROM workflows ORDER BY datetime(created_at) DESC LIMIT -1 OFFSET @offset`)
218
+ .all({ offset: keepLatest });
219
+ for (const row of overflowRows)
220
+ idsToDelete.add(row.id);
221
+ }
222
+ if (idsToDelete.size === 0)
223
+ return { deleted: 0 };
224
+ const deleteStep = db.prepare('DELETE FROM agent_steps WHERE workflow_id = ?');
225
+ const deleteWorkflow = db.prepare('DELETE FROM workflows WHERE id = ?');
226
+ const txn = db.transaction(() => {
227
+ for (const id of idsToDelete) {
228
+ deleteStep.run(id);
229
+ deleteWorkflow.run(id);
230
+ }
231
+ });
232
+ txn();
233
+ return { deleted: idsToDelete.size };
234
+ }
235
+ export function getWorkflowMetrics() {
236
+ var _a, _b, _c, _d, _e, _f, _g;
237
+ const db = getDb();
238
+ const workflowRow = db.prepare(`
239
+ SELECT
240
+ COUNT(*) as workflow_count,
241
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_count,
242
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_count,
243
+ AVG(
244
+ CASE
245
+ WHEN completed_at IS NOT NULL
246
+ THEN (julianday(completed_at) - julianday(created_at)) * 86400000
247
+ ELSE NULL
248
+ END
249
+ ) as avg_workflow_duration_ms
250
+ FROM workflows
251
+ `).get();
252
+ const stepRow = db.prepare(`
253
+ SELECT
254
+ AVG(duration_ms) as avg_step_duration_ms,
255
+ SUM(COALESCE(tokens_in, 0)) as total_tokens_in,
256
+ SUM(COALESCE(tokens_out, 0)) as total_tokens_out
257
+ FROM agent_steps
258
+ `).get();
259
+ return {
260
+ workflowCount: Number((_a = workflowRow === null || workflowRow === void 0 ? void 0 : workflowRow.workflow_count) !== null && _a !== void 0 ? _a : 0),
261
+ completedCount: Number((_b = workflowRow === null || workflowRow === void 0 ? void 0 : workflowRow.completed_count) !== null && _b !== void 0 ? _b : 0),
262
+ failedCount: Number((_c = workflowRow === null || workflowRow === void 0 ? void 0 : workflowRow.failed_count) !== null && _c !== void 0 ? _c : 0),
263
+ avgWorkflowDurationMs: Number((_d = workflowRow === null || workflowRow === void 0 ? void 0 : workflowRow.avg_workflow_duration_ms) !== null && _d !== void 0 ? _d : 0),
264
+ avgStepDurationMs: Number((_e = stepRow === null || stepRow === void 0 ? void 0 : stepRow.avg_step_duration_ms) !== null && _e !== void 0 ? _e : 0),
265
+ totalTokensIn: Number((_f = stepRow === null || stepRow === void 0 ? void 0 : stepRow.total_tokens_in) !== null && _f !== void 0 ? _f : 0),
266
+ totalTokensOut: Number((_g = stepRow === null || stepRow === void 0 ? void 0 : stepRow.total_tokens_out) !== null && _g !== void 0 ? _g : 0),
267
+ };
268
+ }
@@ -0,0 +1,238 @@
1
+ export const SUPPORTED_LOCALES = ["en", "zh-TW"];
2
+ export const DEFAULT_LOCALE = "en";
3
+ export const messages = {
4
+ en: {
5
+ "app.title": "Claude Dashboard",
6
+ "nav.dashboard": "Dashboard",
7
+ "nav.history": "History",
8
+ "nav.events": "Events",
9
+ "nav.terminal": "Terminal",
10
+ "nav.toggleEvents": "Toggle event log panel",
11
+ "nav.toggleTerminal": "Toggle terminal panel",
12
+ "nav.agentsProgress": "{completed}/{total} agents",
13
+ "nav.language": "Language",
14
+ "launcher.templates": "Templates:",
15
+ "launcher.placeholder": "Describe your development task...",
16
+ "launcher.start": "Start",
17
+ "launcher.pause": "Pause",
18
+ "launcher.resume": "Resume",
19
+ "launcher.cancel": "Cancel",
20
+ "launcher.preview": "Preview",
21
+ "launcher.runMode": "Run mode:",
22
+ "launcher.mode.full": "Full (PM/RD/UI/TEST/SEC)",
23
+ "launcher.mode.fast": "Fast (skip TEST/SEC)",
24
+ "launcher.mode.custom": "Custom",
25
+ "launcher.warning.skip": "⚠️ This run will skip {roles}.",
26
+ "launcher.impact": "Impact: {level}",
27
+ "launcher.suggestedMode": "Suggested mode: {mode}",
28
+ "launcher.applySuggestion": "Apply suggestion",
29
+ "launcher.impact.low.reason": "Looks like a small scoped task. Fast mode is probably sufficient.",
30
+ "launcher.impact.medium.reason": "Likely multi-file change. Consider Full mode or at least include TEST.",
31
+ "launcher.impact.high.reason": "Prompt touches sensitive or high-risk areas. Keep TEST and SEC enabled.",
32
+ "history.title": "Workflow History",
33
+ "history.filters.allStatuses": "All statuses",
34
+ "history.filters.searchPlaceholder": "Search title or prompt",
35
+ "history.filters.search": "Search",
36
+ "history.retention": "Retention cleanup:",
37
+ "history.keepDays": "keep days",
38
+ "history.keepLatest": "keep latest",
39
+ "history.runCleanup": "Run Cleanup",
40
+ "history.loading": "Loading...",
41
+ "history.empty": "No workflows found.",
42
+ "history.table.title": "Title",
43
+ "history.table.status": "Status",
44
+ "history.table.created": "Created",
45
+ "history.table.duration": "Duration",
46
+ "history.pagination.prev": "Prev",
47
+ "history.pagination.next": "Next",
48
+ "history.metrics.workflows": "Workflows",
49
+ "history.metrics.completedFailed": "Completed / Failed",
50
+ "history.metrics.avgWorkflow": "Avg workflow",
51
+ "history.metrics.avgStep": "Avg step",
52
+ "history.metrics.tokensIn": "Tokens in",
53
+ "history.metrics.tokensOut": "Tokens out",
54
+ "history.pagination.summary": "{total} total · page {page}/{totalPages}",
55
+ "history.loadFailed": "Failed to load workflows: {error}",
56
+ "history.cleanup.confirm": "Cleanup history with policy: keep {keepDays} days OR latest {keepLatest} workflows?",
57
+ "history.cleanup.failed": "Cleanup failed",
58
+ "history.cleanup.success": "Cleanup complete: deleted {count} workflows.",
59
+ "pipeline.done": "Done",
60
+ "status.pending": "Pending",
61
+ "status.running": "Running",
62
+ "status.paused": "Paused",
63
+ "status.completed": "Done",
64
+ "status.failed": "Failed",
65
+ "status.skipped": "Skipped",
66
+ "status.cancelled": "Cancelled",
67
+ "agent.retry": "retry {count}",
68
+ "agent.tokens.in": "In: {value}",
69
+ "agent.tokens.out": "Out: {value}",
70
+ "agent.output.none": "No output yet",
71
+ "agent.activity.thinking": "Thinking...",
72
+ "agent.activity.toolUse": "Using {tool}...",
73
+ "agent.activity.writing": "Writing response...",
74
+ "agent.activity.waiting": "Waiting for output...",
75
+ "events.title": "Event Log",
76
+ "events.count": "{count} events",
77
+ "events.connected": "Connected to server",
78
+ "events.workflowStarted": "Workflow started: {title}",
79
+ "events.workflowCompleted": "Workflow completed successfully",
80
+ "events.workflowFailed": "Workflow failed: {error}",
81
+ "events.workflowPaused": "Workflow paused",
82
+ "events.workflowCancelled": "Workflow cancelled",
83
+ "events.agentStarted": "{agent} agent started{retry}",
84
+ "events.agentCompleted": "{agent} agent completed in {seconds}s",
85
+ "events.agentFailed": "{agent} agent failed: {error}",
86
+ "events.agentRetry": "{agent} agent retrying ({attempt}/{max}): {reason}",
87
+ "events.retrySuffix": " (retry {count})",
88
+ "usage.loadingAria": "Loading usage data",
89
+ "usage.unavailableAria": "Usage data unavailable",
90
+ "usage.unavailable": "Usage: --",
91
+ "usage.session": "Session",
92
+ "usage.week": "Week",
93
+ "usage.sonnet": "Sonnet",
94
+ "usage.tooltip.session": "Current Session (5h window)",
95
+ "usage.tooltip.week": "Weekly All Models (7 days)",
96
+ "usage.tooltip.sonnet": "Weekly Sonnet (7 days)",
97
+ "usage.tooltip.usage": "Usage: {value}",
98
+ "usage.tooltip.resets": "Resets: {value}",
99
+ "usage.tooltip.na": "N/A",
100
+ "terminal.title": "Terminal",
101
+ "terminal.connecting": "Connecting to server...",
102
+ "terminal.connected": "Connected",
103
+ "terminal.initializing": "Initializing...",
104
+ "terminal.error": "Error",
105
+ "historyDetail.title": "Workflow Detail",
106
+ "historyDetail.back": "← Back to History",
107
+ "historyDetail.loading": "Loading...",
108
+ "historyDetail.loadFailed": "Failed to load: {error}",
109
+ "historyDetail.meta.title": "Title:",
110
+ "historyDetail.meta.status": "Status:",
111
+ "historyDetail.meta.created": "Created:",
112
+ "historyDetail.meta.completed": "Completed:",
113
+ "historyDetail.retryWorkflow": "Retry entire workflow as new run",
114
+ "historyDetail.artifacts.title": "Run artifact summary (detected file paths)",
115
+ "historyDetail.artifacts.empty": "No file paths detected from agent outputs.",
116
+ "historyDetail.step.retry": "Retry from this step as new run",
117
+ "historyDetail.step.noOutput": "(no output)",
118
+ "historyDetail.step.in": "in",
119
+ "historyDetail.step.out": "out",
120
+ },
121
+ "zh-TW": {
122
+ "app.title": "Claude 儀表板",
123
+ "nav.dashboard": "儀表板",
124
+ "nav.history": "歷史",
125
+ "nav.events": "事件",
126
+ "nav.terminal": "終端機",
127
+ "nav.toggleEvents": "切換事件面板",
128
+ "nav.toggleTerminal": "切換終端機面板",
129
+ "nav.agentsProgress": "{completed}/{total} 個代理",
130
+ "nav.language": "語言",
131
+ "launcher.templates": "模板:",
132
+ "launcher.placeholder": "描述你的開發任務...",
133
+ "launcher.start": "開始",
134
+ "launcher.pause": "暫停",
135
+ "launcher.resume": "繼續",
136
+ "launcher.cancel": "取消",
137
+ "launcher.preview": "預估",
138
+ "launcher.runMode": "執行模式:",
139
+ "launcher.mode.full": "完整(PM/RD/UI/TEST/SEC)",
140
+ "launcher.mode.fast": "快速(略過 TEST/SEC)",
141
+ "launcher.mode.custom": "自訂",
142
+ "launcher.warning.skip": "⚠️ 此次執行將略過 {roles}。",
143
+ "launcher.impact": "影響:{level}",
144
+ "launcher.suggestedMode": "建議模式:{mode}",
145
+ "launcher.applySuggestion": "套用建議",
146
+ "launcher.impact.low.reason": "看起來是小範圍任務,快速模式應該足夠。",
147
+ "launcher.impact.medium.reason": "可能會動到多個檔案,建議完整模式或至少包含 TEST。",
148
+ "launcher.impact.high.reason": "涉及敏感或高風險區域,建議保留 TEST 與 SEC。",
149
+ "history.title": "工作流程歷史",
150
+ "history.filters.allStatuses": "所有狀態",
151
+ "history.filters.searchPlaceholder": "搜尋標題或提示詞",
152
+ "history.filters.search": "搜尋",
153
+ "history.retention": "保留清理:",
154
+ "history.keepDays": "保留天數",
155
+ "history.keepLatest": "保留最新",
156
+ "history.runCleanup": "執行清理",
157
+ "history.loading": "載入中...",
158
+ "history.empty": "找不到工作流程。",
159
+ "history.table.title": "標題",
160
+ "history.table.status": "狀態",
161
+ "history.table.created": "建立時間",
162
+ "history.table.duration": "耗時",
163
+ "history.pagination.prev": "上一頁",
164
+ "history.pagination.next": "下一頁",
165
+ "history.metrics.workflows": "工作流程數",
166
+ "history.metrics.completedFailed": "完成 / 失敗",
167
+ "history.metrics.avgWorkflow": "平均流程",
168
+ "history.metrics.avgStep": "平均步驟",
169
+ "history.metrics.tokensIn": "輸入 tokens",
170
+ "history.metrics.tokensOut": "輸出 tokens",
171
+ "history.pagination.summary": "共 {total} 筆 · 第 {page}/{totalPages} 頁",
172
+ "history.loadFailed": "載入工作流程失敗:{error}",
173
+ "history.cleanup.confirm": "清理策略:保留 {keepDays} 天 或 最新 {keepLatest} 筆工作流程,確定執行?",
174
+ "history.cleanup.failed": "清理失敗",
175
+ "history.cleanup.success": "清理完成:刪除 {count} 筆工作流程。",
176
+ "pipeline.done": "完成",
177
+ "status.pending": "待處理",
178
+ "status.running": "執行中",
179
+ "status.paused": "已暫停",
180
+ "status.completed": "完成",
181
+ "status.failed": "失敗",
182
+ "status.skipped": "略過",
183
+ "status.cancelled": "已取消",
184
+ "agent.retry": "重試 {count}",
185
+ "agent.tokens.in": "輸入: {value}",
186
+ "agent.tokens.out": "輸出: {value}",
187
+ "agent.output.none": "尚無輸出",
188
+ "agent.activity.thinking": "思考中...",
189
+ "agent.activity.toolUse": "使用 {tool} 中...",
190
+ "agent.activity.writing": "撰寫回應中...",
191
+ "agent.activity.waiting": "等待輸出中...",
192
+ "events.title": "事件記錄",
193
+ "events.count": "{count} 筆事件",
194
+ "events.connected": "已連線到伺服器",
195
+ "events.workflowStarted": "工作流程已開始:{title}",
196
+ "events.workflowCompleted": "工作流程已成功完成",
197
+ "events.workflowFailed": "工作流程失敗:{error}",
198
+ "events.workflowPaused": "工作流程已暫停",
199
+ "events.workflowCancelled": "工作流程已取消",
200
+ "events.agentStarted": "{agent} 代理已開始{retry}",
201
+ "events.agentCompleted": "{agent} 代理已於 {seconds}s 完成",
202
+ "events.agentFailed": "{agent} 代理失敗:{error}",
203
+ "events.agentRetry": "{agent} 代理重試中({attempt}/{max}):{reason}",
204
+ "events.retrySuffix": "(重試 {count})",
205
+ "usage.loadingAria": "載入使用量資料中",
206
+ "usage.unavailableAria": "使用量資料不可用",
207
+ "usage.unavailable": "使用量: --",
208
+ "usage.session": "本次",
209
+ "usage.week": "本週",
210
+ "usage.sonnet": "Sonnet",
211
+ "usage.tooltip.session": "目前會話(5 小時視窗)",
212
+ "usage.tooltip.week": "全模型週使用量(7 天)",
213
+ "usage.tooltip.sonnet": "Sonnet 週使用量(7 天)",
214
+ "usage.tooltip.usage": "使用率: {value}",
215
+ "usage.tooltip.resets": "重置: {value}",
216
+ "usage.tooltip.na": "無",
217
+ "terminal.title": "終端機",
218
+ "terminal.connecting": "連線到伺服器中...",
219
+ "terminal.connected": "已連線",
220
+ "terminal.initializing": "初始化中...",
221
+ "terminal.error": "錯誤",
222
+ "historyDetail.title": "工作流程詳情",
223
+ "historyDetail.back": "← 返回歷史",
224
+ "historyDetail.loading": "載入中...",
225
+ "historyDetail.loadFailed": "載入失敗:{error}",
226
+ "historyDetail.meta.title": "標題:",
227
+ "historyDetail.meta.status": "狀態:",
228
+ "historyDetail.meta.created": "建立時間:",
229
+ "historyDetail.meta.completed": "完成時間:",
230
+ "historyDetail.retryWorkflow": "重試整個工作流程(新執行)",
231
+ "historyDetail.artifacts.title": "執行產物摘要(偵測到的檔案路徑)",
232
+ "historyDetail.artifacts.empty": "未從代理輸出偵測到檔案路徑。",
233
+ "historyDetail.step.retry": "從此步驟重試(新執行)",
234
+ "historyDetail.step.noOutput": "(無輸出)",
235
+ "historyDetail.step.in": "輸入",
236
+ "historyDetail.step.out": "輸出",
237
+ },
238
+ };
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { useCallback } from "react";
3
+ import { useI18nStore } from "@/stores/i18nStore";
4
+ import { DEFAULT_LOCALE, messages } from "@/lib/i18n/messages";
5
+ function interpolate(template, vars) {
6
+ if (!vars)
7
+ return template;
8
+ return template.replace(/\{(\w+)\}/g, (_, key) => { var _a; return String((_a = vars[key]) !== null && _a !== void 0 ? _a : `{${key}}`); });
9
+ }
10
+ export function useI18n() {
11
+ var _a;
12
+ const locale = useI18nStore((s) => s.locale);
13
+ const dict = (_a = messages[locale]) !== null && _a !== void 0 ? _a : messages[DEFAULT_LOCALE];
14
+ const t = useCallback((key, vars) => {
15
+ var _a, _b;
16
+ const raw = (_b = (_a = dict[key]) !== null && _a !== void 0 ? _a : messages[DEFAULT_LOCALE][key]) !== null && _b !== void 0 ? _b : key;
17
+ return interpolate(raw, vars);
18
+ }, [dict]);
19
+ return { locale, t };
20
+ }
@@ -34,6 +34,7 @@ function getShellPath() {
34
34
  return preferred;
35
35
  return '/bin/sh';
36
36
  }
37
+ const MAX_REPLAY_BUFFER = 200000;
37
38
  export class PtyManager {
38
39
  constructor() {
39
40
  this.sessions = new Map();
@@ -51,15 +52,27 @@ export class PtyManager {
51
52
  cwd,
52
53
  env: Object.assign({}, process.env),
53
54
  });
55
+ const session = { id, process: proc, onData, buffer: '' };
54
56
  proc.onData((data) => {
55
- onData(data);
57
+ session.buffer += data;
58
+ if (session.buffer.length > MAX_REPLAY_BUFFER) {
59
+ session.buffer = session.buffer.slice(session.buffer.length - MAX_REPLAY_BUFFER);
60
+ }
61
+ session.onData(data);
56
62
  });
57
63
  proc.onExit(() => {
58
64
  this.sessions.delete(id);
59
65
  });
60
- this.sessions.set(id, { id, process: proc, onData });
66
+ this.sessions.set(id, session);
61
67
  return id;
62
68
  }
69
+ attach(id, onData) {
70
+ const session = this.sessions.get(id);
71
+ if (!session)
72
+ return { ok: false };
73
+ session.onData = onData;
74
+ return { ok: true, replay: session.buffer };
75
+ }
63
76
  write(id, data) {
64
77
  const session = this.sessions.get(id);
65
78
  if (session) {
@@ -1,9 +1,10 @@
1
+ import { normalizeExecutionPlan } from "../workflow/types.js";
1
2
  export function setupWebSocketHandlers(wss, connectionManager, engine, ptyManager, projectPath) {
2
3
  // Wire workflow engine events to WebSocket broadcasts
3
- engine.on('workflow:created', (workflowId, title) => {
4
+ engine.on('workflow:created', (workflowId, title, executionPlan) => {
4
5
  connectionManager.broadcastAll({
5
6
  type: 'workflow:created',
6
- payload: { workflowId, title },
7
+ payload: { workflowId, title, executionPlan },
7
8
  });
8
9
  });
9
10
  engine.on('workflow:completed', (workflowId) => {
@@ -94,7 +95,7 @@ function handleClientMessage(clientId, ws, msg, connectionManager, engine, ptyMa
94
95
  ws.send(JSON.stringify({ type: 'pong' }));
95
96
  break;
96
97
  case 'workflow:start': {
97
- const { prompt, projectPath } = msg.payload || {};
98
+ const { prompt, projectPath, executionPlan } = msg.payload || {};
98
99
  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
99
100
  ws.send(JSON.stringify({
100
101
  type: 'workflow:failed',
@@ -103,7 +104,8 @@ function handleClientMessage(clientId, ws, msg, connectionManager, engine, ptyMa
103
104
  break;
104
105
  }
105
106
  const path = projectPath || defaultProjectPath;
106
- engine.startWorkflow(prompt, path).then((workflowId) => {
107
+ const normalizedPlan = normalizeExecutionPlan(Array.isArray(executionPlan) ? executionPlan : undefined);
108
+ engine.startWorkflow(prompt, path, normalizedPlan).then((workflowId) => {
107
109
  connectionManager.subscribeToWorkflow(clientId, workflowId);
108
110
  }).catch((err) => {
109
111
  console.error('[WS] Failed to start workflow:', err);
@@ -163,6 +165,40 @@ function handleClientMessage(clientId, ws, msg, connectionManager, engine, ptyMa
163
165
  }
164
166
  break;
165
167
  }
168
+ case 'terminal:attach': {
169
+ const { terminalId, refresh } = msg.payload || {};
170
+ if (!terminalId)
171
+ break;
172
+ const result = ptyManager.attach(terminalId, (data) => {
173
+ connectionManager.sendTo(clientId, {
174
+ type: 'terminal:output',
175
+ payload: { terminalId, data },
176
+ });
177
+ });
178
+ if (result.ok) {
179
+ connectionManager.sendTo(clientId, {
180
+ type: 'terminal:created',
181
+ payload: { terminalId },
182
+ });
183
+ if (result.replay) {
184
+ connectionManager.sendTo(clientId, {
185
+ type: 'terminal:output',
186
+ payload: { terminalId, data: result.replay },
187
+ });
188
+ }
189
+ if (refresh) {
190
+ // Trigger a full-screen redraw for TUI apps (e.g. htop) after reattach.
191
+ ptyManager.write(terminalId, '\x0c');
192
+ }
193
+ }
194
+ else {
195
+ connectionManager.sendTo(clientId, {
196
+ type: 'terminal:error',
197
+ payload: { error: 'Terminal session not found; create a new one.' },
198
+ });
199
+ }
200
+ break;
201
+ }
166
202
  case 'terminal:input': {
167
203
  const { terminalId, data } = msg.payload || {};
168
204
  if (terminalId && data != null) {
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { v4 as uuidv4 } from 'uuid';
3
- import { AGENT_CONFIG, PIPELINE_STAGES, getStageForRole } from "./types.js";
3
+ import { AGENT_CONFIG, PIPELINE_STAGES, getStageForRole, normalizeExecutionPlan } from "./types.js";
4
4
  import { createPipelineState } from "./pipeline.js";
5
5
  import { AgentRunner } from "./agent-runner.js";
6
6
  import { buildAgentPrompt } from "./context-builder.js";
@@ -17,11 +17,12 @@ export class WorkflowEngine extends EventEmitter {
17
17
  this.paused = new Set();
18
18
  this.dbOps = dbOps;
19
19
  }
20
- async startWorkflow(userPrompt, projectPath) {
20
+ async startWorkflow(userPrompt, projectPath, executionPlan) {
21
21
  const workflowId = uuidv4();
22
22
  const title = userPrompt.slice(0, 80) + (userPrompt.length > 80 ? '...' : '');
23
- this.dbOps.createWorkflow(workflowId, title, userPrompt, projectPath);
24
- const pipeline = createPipelineState(workflowId);
23
+ const normalizedPlan = normalizeExecutionPlan(executionPlan);
24
+ this.dbOps.createWorkflow(workflowId, title, userPrompt, projectPath, normalizedPlan);
25
+ const pipeline = createPipelineState(workflowId, normalizedPlan);
25
26
  pipeline.status = 'running';
26
27
  this.pipelines.set(workflowId, pipeline);
27
28
  this.stepOutputs.set(workflowId, new Map());
@@ -33,20 +34,25 @@ export class WorkflowEngine extends EventEmitter {
33
34
  }
34
35
  this.stepIds.set(workflowId, stepIdMap);
35
36
  this.dbOps.updateWorkflowStatus(workflowId, 'running');
36
- this.emit('workflow:created', workflowId, title);
37
+ this.emit('workflow:created', workflowId, title, normalizedPlan);
37
38
  // Defer to macrotask so callers can subscribe (microtask) before first step:started fires
38
39
  setTimeout(() => {
39
- this.executePipeline(workflowId, userPrompt, projectPath).catch((err) => {
40
+ this.executePipeline(workflowId, userPrompt, projectPath, normalizedPlan).catch((err) => {
40
41
  console.error(`Workflow ${workflowId} failed:`, err);
41
42
  });
42
43
  }, 0);
43
44
  return workflowId;
44
45
  }
45
- async executePipeline(workflowId, userPrompt, projectPath) {
46
+ async executePipeline(workflowId, userPrompt, projectPath, executionPlan) {
46
47
  const pipeline = this.pipelines.get(workflowId);
47
48
  if (!pipeline)
48
49
  return;
50
+ const selected = new Set(executionPlan);
49
51
  for (const stage of PIPELINE_STAGES) {
52
+ const stageRoles = stage.roles.filter((role) => selected.has(role));
53
+ if (stageRoles.length === 0) {
54
+ continue;
55
+ }
50
56
  // Check if cancelled
51
57
  if (pipeline.status === 'cancelled') {
52
58
  this.cleanupWorkflow(workflowId);
@@ -63,12 +69,12 @@ export class WorkflowEngine extends EventEmitter {
63
69
  pipeline.currentStageIndex = stage.index;
64
70
  this.dbOps.updateWorkflowStatus(workflowId, 'running', stage.index);
65
71
  // Execute all roles in this stage in parallel
66
- const results = await Promise.allSettled(stage.roles.map(role => this.executeStep(workflowId, role, userPrompt, projectPath)));
72
+ const results = await Promise.allSettled(stageRoles.map(role => this.executeStep(workflowId, role, userPrompt, projectPath)));
67
73
  // Check for failures after all peers finish
68
74
  const failedRoles = [];
69
75
  for (let i = 0; i < results.length; i++) {
70
76
  const result = results[i];
71
- const role = stage.roles[i];
77
+ const role = stageRoles[i];
72
78
  const stepState = pipeline.steps.find(s => s.role === role);
73
79
  if (result.status === 'fulfilled' && result.value) {
74
80
  if (stepState)
@@ -1,12 +1,13 @@
1
- import { AGENT_ORDER } from "./types.js";
2
- export function createPipelineState(workflowId) {
1
+ import { AGENT_ORDER, normalizeExecutionPlan } from "./types.js";
2
+ export function createPipelineState(workflowId, executionPlan) {
3
+ const selected = new Set(normalizeExecutionPlan(executionPlan));
3
4
  return {
4
5
  workflowId,
5
6
  status: 'pending',
6
7
  currentStageIndex: 0,
7
8
  steps: AGENT_ORDER.map((role) => ({
8
9
  role,
9
- status: 'pending',
10
+ status: selected.has(role) ? 'pending' : 'skipped',
10
11
  retryCount: 0,
11
12
  })),
12
13
  };
@@ -1,3 +1,4 @@
1
+ export const ALL_AGENT_ROLES = ['pm', 'rd', 'ui', 'test', 'sec'];
1
2
  export const PIPELINE_STAGES = [
2
3
  { index: 0, roles: ['pm'] },
3
4
  { index: 1, roles: ['rd', 'ui'] },
@@ -5,6 +6,18 @@ export const PIPELINE_STAGES = [
5
6
  ];
6
7
  /** The fixed execution order for agents within a workflow (derived from stages). */
7
8
  export const AGENT_ORDER = PIPELINE_STAGES.flatMap(s => s.roles);
9
+ export function normalizeExecutionPlan(input) {
10
+ if (!input || input.length === 0)
11
+ return [...AGENT_ORDER];
12
+ const requested = new Set();
13
+ for (const role of input) {
14
+ if (AGENT_ORDER.includes(role)) {
15
+ requested.add(role);
16
+ }
17
+ }
18
+ const ordered = AGENT_ORDER.filter((role) => requested.has(role));
19
+ return ordered.length > 0 ? ordered : [...AGENT_ORDER];
20
+ }
8
21
  /** Get the pipeline stage that contains the given role. */
9
22
  export function getStageForRole(role) {
10
23
  const stage = PIPELINE_STAGES.find(s => s.roles.includes(role));