claude-teammate 0.1.261 → 0.1.263
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.
- package/package.json +1 -1
- package/src/claude.js +35 -0
- package/src/dashboard/app/components/ExecutionQueue.vue +5 -5
- package/src/dashboard/app/components/IssueDrawer.vue +12 -12
- package/src/dashboard/app/components/LogPanel.vue +1 -1
- package/src/dashboard/app/components/MemTreeNode.vue +5 -5
- package/src/dashboard/app/layouts/default.vue +6 -6
- package/src/dashboard/app/pages/activity.vue +1 -1
- package/src/dashboard/app/pages/awaiting.vue +2 -2
- package/src/dashboard/app/pages/config.vue +1 -1
- package/src/dashboard/app/pages/github-issues.vue +1 -1
- package/src/dashboard/app/pages/index.vue +22 -22
- package/src/dashboard/app/pages/logs.vue +3 -3
- package/src/dashboard/app/pages/memory.vue +4 -4
- package/src/dashboard/app/pages/pipeline.vue +4 -4
- package/src/dashboard/app/pages/repos.vue +1 -1
- package/src/dashboard/app/pages/usage.vue +25 -25
- package/src/worker/github-issue-workflow.js +4 -2
- package/src/worker/pull-request.js +27 -8
package/package.json
CHANGED
package/src/claude.js
CHANGED
|
@@ -820,3 +820,38 @@ export async function runClaudeRepoExtraction(input) {
|
|
|
820
820
|
}
|
|
821
821
|
);
|
|
822
822
|
}
|
|
823
|
+
|
|
824
|
+
const BRANCH_SLUG_SCHEMA = {
|
|
825
|
+
type: "object",
|
|
826
|
+
properties: {
|
|
827
|
+
slug: { type: "string" }
|
|
828
|
+
},
|
|
829
|
+
required: ["slug"],
|
|
830
|
+
additionalProperties: false
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
export async function runClaudeBranchSlug(title) {
|
|
834
|
+
try {
|
|
835
|
+
const result = await invokeClaudeTask(
|
|
836
|
+
BRANCH_SLUG_SCHEMA,
|
|
837
|
+
"Generate a short English branch name slug: 3-5 lowercase words joined by hyphens that summarize the issue title. Output only the slug field. Example outputs: 'add-user-auth', 'fix-login-redirect', 'manage-maker-account-requests'.",
|
|
838
|
+
`Issue title: ${title}`,
|
|
839
|
+
{
|
|
840
|
+
model: "haiku",
|
|
841
|
+
permissionMode: "bypassPermissions",
|
|
842
|
+
effort: "low",
|
|
843
|
+
runOpts: {
|
|
844
|
+
timeout: 15_000,
|
|
845
|
+
phase: "branch-slug"
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
return String(result?.slug || "")
|
|
850
|
+
.toLowerCase()
|
|
851
|
+
.replace(/[^a-z0-9-]/gu, "")
|
|
852
|
+
.replace(/^-+|-+$/gu, "")
|
|
853
|
+
.slice(0, 40);
|
|
854
|
+
} catch {
|
|
855
|
+
return "";
|
|
856
|
+
}
|
|
857
|
+
}
|
|
@@ -68,9 +68,9 @@ defineEmits<{
|
|
|
68
68
|
"open-drawer": [issue: WorkflowIssue];
|
|
69
69
|
}>();
|
|
70
70
|
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
71
|
+
const _running = computed(() => props.status?.state?.workQueue?.running || []);
|
|
72
|
+
const _queued = computed(() => props.status?.state?.workQueue?.queued || []);
|
|
73
|
+
const _limit = computed(() => props.status?.state?.workQueue?.limit || 0);
|
|
74
74
|
|
|
75
75
|
const issueIndex = computed(() => {
|
|
76
76
|
const issues = [...(props.status?.state?.allIssues || []), ...(props.status?.state?.issues || [])];
|
|
@@ -81,7 +81,7 @@ const issueIndex = computed(() => {
|
|
|
81
81
|
return map;
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
function
|
|
84
|
+
function _getIssue(key: string | undefined) {
|
|
85
85
|
if (!key) return null;
|
|
86
86
|
return issueIndex.value.get(String(key).trim()) || null;
|
|
87
87
|
}
|
|
@@ -101,7 +101,7 @@ function forgeRepoName(url: string | undefined): string {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function
|
|
104
|
+
function _itemLabel(item: {
|
|
105
105
|
kind?: string;
|
|
106
106
|
key?: string;
|
|
107
107
|
url?: string;
|
|
@@ -254,7 +254,7 @@ const logLoading = ref(false);
|
|
|
254
254
|
const logFeed = ref<HTMLElement | null>(null);
|
|
255
255
|
let logSource: EventSource | null = null;
|
|
256
256
|
|
|
257
|
-
const
|
|
257
|
+
const _filters = [
|
|
258
258
|
{ key: "all", label: "All" },
|
|
259
259
|
{ key: "thinking", label: "Thinking" },
|
|
260
260
|
{ key: "tool", label: "Tools" },
|
|
@@ -285,15 +285,15 @@ const STAGE_COLORS: Record<string, string> = {
|
|
|
285
285
|
};
|
|
286
286
|
|
|
287
287
|
const stagePct = computed(() => STAGE_PCT[props.issue?.workflowState || ""] || 0);
|
|
288
|
-
const
|
|
288
|
+
const _stageColor = computed(() => STAGE_COLORS[props.issue?.workflowState || ""] || "var(--sky)");
|
|
289
289
|
const progressWidth = ref(0);
|
|
290
290
|
|
|
291
|
-
const
|
|
291
|
+
const _draftPrs = computed((): DraftPr[] => {
|
|
292
292
|
if (!props.issue || !props.allData) return [];
|
|
293
293
|
return (props.allData.state?.draftPrs || []).filter((p) => (props.issue!.repoUrls || []).includes(p.repoUrl));
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
-
const
|
|
296
|
+
const _trackedGhIssues = computed((): GithubIssue[] => {
|
|
297
297
|
if (!props.issue || !props.allData) return [];
|
|
298
298
|
return (props.allData.state?.githubIssues || []).filter((g) => (props.issue!.repoUrls || []).includes(g.repoUrl));
|
|
299
299
|
});
|
|
@@ -330,12 +330,12 @@ const enrichedRepos = computed((): EnrichedRepo[] => {
|
|
|
330
330
|
});
|
|
331
331
|
});
|
|
332
332
|
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
const
|
|
333
|
+
const _primaryRepo = computed(() => enrichedRepos.value.find((r) => r.isPr) || null);
|
|
334
|
+
const _workingRepos = computed(() => enrichedRepos.value.filter((r) => !r.isPr && r.branch));
|
|
335
|
+
const _referenceRepos = computed(() => enrichedRepos.value.filter((r) => !r.isPr && !r.branch));
|
|
336
336
|
|
|
337
337
|
// ── Log ─────────────────────────────────────────────────
|
|
338
|
-
function
|
|
338
|
+
function _isEntryVisible(type: string): boolean {
|
|
339
339
|
if (logFilter.value === "all") return true;
|
|
340
340
|
if (logFilter.value === "thinking") return type === "thinking";
|
|
341
341
|
if (logFilter.value === "tool") return type === "tool";
|
|
@@ -343,7 +343,7 @@ function isEntryVisible(type: string): boolean {
|
|
|
343
343
|
return true;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
function
|
|
346
|
+
function _setLogFilter(f: string) {
|
|
347
347
|
logFilter.value = f;
|
|
348
348
|
}
|
|
349
349
|
|
|
@@ -488,11 +488,11 @@ function disconnectLogStream() {
|
|
|
488
488
|
}
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
function
|
|
491
|
+
function _refreshLog() {
|
|
492
492
|
if (props.issue?.key) connectLogStream(props.issue.key);
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
-
function
|
|
495
|
+
function _downloadLog() {
|
|
496
496
|
if (!props.issue?.key) return;
|
|
497
497
|
const a = document.createElement("a");
|
|
498
498
|
a.href = `/api/issue-logs/${encodeURIComponent(props.issue.key)}/download`;
|
|
@@ -502,7 +502,7 @@ function downloadLog() {
|
|
|
502
502
|
document.body.removeChild(a);
|
|
503
503
|
}
|
|
504
504
|
|
|
505
|
-
async function
|
|
505
|
+
async function _resetIssue() {
|
|
506
506
|
if (!props.issue?.key) return;
|
|
507
507
|
if (!confirm(`Reset "${props.issue.key}" back to Todo? This will clear all task state and labels.`)) return;
|
|
508
508
|
resetting.value = true;
|
|
@@ -32,7 +32,7 @@ watch(
|
|
|
32
32
|
}
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
-
function
|
|
35
|
+
function _renderLine(line: string): string {
|
|
36
36
|
// Strip the [tag] segment from display — shown via tab label instead
|
|
37
37
|
const clean = line.replace(/^(\[[^\]]+\] (?:INFO|ERROR) )\[([a-z:-]+)\] /, "$1");
|
|
38
38
|
const tsRaw = (clean.match(/^\[([^\]]+)\]/) || [])[1] || "";
|
|
@@ -52,30 +52,30 @@ const emit = defineEmits<{
|
|
|
52
52
|
"toggle-dir": [path: string];
|
|
53
53
|
}>();
|
|
54
54
|
|
|
55
|
-
const
|
|
55
|
+
const _fmtBytes = inject<(b: number) => string>("fmtBytes", (b) => String(b));
|
|
56
56
|
|
|
57
57
|
const isExpanded = computed(() => props.expanded.has(props.node.path));
|
|
58
58
|
|
|
59
|
-
const
|
|
59
|
+
const _icon = computed(() => {
|
|
60
60
|
if (props.node.isDir) return isExpanded.value ? "▾" : "▸";
|
|
61
61
|
return "·";
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
const
|
|
64
|
+
const _nodeClass = computed(() => {
|
|
65
65
|
if (props.node.path === props.selectedPath) return "selected";
|
|
66
66
|
// ancestor check
|
|
67
67
|
if (props.selectedPath && props.selectedPath.startsWith(props.node.path + "/")) return "ancestor";
|
|
68
68
|
return "";
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
const
|
|
71
|
+
const _sortedChildren = computed(() => {
|
|
72
72
|
return [...props.node.children.values()].sort((a, b) => {
|
|
73
73
|
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
74
74
|
return a.name.localeCompare(b.name);
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
function
|
|
78
|
+
function _handleClick() {
|
|
79
79
|
if (props.node.isDir) emit("toggle-dir", props.node.path);
|
|
80
80
|
else emit("select-file", props.node.path);
|
|
81
81
|
}
|
|
@@ -158,31 +158,31 @@ const { status, startPolling } = useStatus();
|
|
|
158
158
|
const sidebarOpen = ref(false);
|
|
159
159
|
const clockText = ref("");
|
|
160
160
|
|
|
161
|
-
function
|
|
161
|
+
function _toggleSidebar() {
|
|
162
162
|
sidebarOpen.value = !sidebarOpen.value;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
function
|
|
165
|
+
function _closeSidebar() {
|
|
166
166
|
sidebarOpen.value = false;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
const
|
|
169
|
+
const _workerClass = computed(() => {
|
|
170
170
|
if (!status.value) return "offline";
|
|
171
171
|
return status.value.worker.running ? "online" : "offline";
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
const
|
|
174
|
+
const _workerText = computed(() => {
|
|
175
175
|
if (!status.value) return "Checking…";
|
|
176
176
|
if (status.value.worker.running) return `ONLINE · PID ${status.value.worker.pid}`;
|
|
177
177
|
return "OFFLINE";
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
const
|
|
180
|
+
const _pipelineCount = computed(() => {
|
|
181
181
|
if (!status.value) return null;
|
|
182
182
|
return status.value.state?.issues?.length ?? 0;
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
-
const
|
|
185
|
+
const _awaitingCount = computed(() => {
|
|
186
186
|
if (!status.value) return null;
|
|
187
187
|
const issues = status.value.state?.issues || [];
|
|
188
188
|
const draftPrs = status.value.state?.draftPrs || [];
|
|
@@ -48,7 +48,7 @@ interface ActivityEvent {
|
|
|
48
48
|
color: string;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const
|
|
51
|
+
const _events = computed((): ActivityEvent[] => {
|
|
52
52
|
if (!status.value) return [];
|
|
53
53
|
const s = status.value.state || {};
|
|
54
54
|
const list: ActivityEvent[] = [];
|
|
@@ -158,7 +158,7 @@ const { shortenUrl } = useHelpers();
|
|
|
158
158
|
|
|
159
159
|
const activeBoardFilter = ref<string | null>(null);
|
|
160
160
|
|
|
161
|
-
const
|
|
161
|
+
const _boards = computed(() => {
|
|
162
162
|
const issues = status.value?.state?.issues || [];
|
|
163
163
|
return [...new Set(issues.map((i) => i.projectKey).filter(Boolean))].sort() as string[];
|
|
164
164
|
});
|
|
@@ -180,7 +180,7 @@ const prWait = computed((): DraftPr[] => {
|
|
|
180
180
|
);
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
-
const
|
|
183
|
+
const _total = computed(
|
|
184
184
|
() => clarify.value.length + humanReply.value.length + approval.value.length + repo.value.length + prWait.value.length
|
|
185
185
|
);
|
|
186
186
|
</script>
|
|
@@ -32,7 +32,7 @@ const config = ref<Record<string, string>>({});
|
|
|
32
32
|
const loading = ref(false);
|
|
33
33
|
const error = ref(false);
|
|
34
34
|
|
|
35
|
-
const
|
|
35
|
+
const _configEntries = computed(() => Object.entries(config.value));
|
|
36
36
|
|
|
37
37
|
async function loadConfig() {
|
|
38
38
|
loading.value = true;
|
|
@@ -54,5 +54,5 @@ const { status } = useStatus();
|
|
|
54
54
|
const { stateLabel, shortenUrl } = useHelpers();
|
|
55
55
|
|
|
56
56
|
const githubIssues = computed(() => (status.value?.state?.githubIssues || []) as GithubIssue[]);
|
|
57
|
-
const
|
|
57
|
+
const _repoCount = computed(() => new Set(githubIssues.value.map((g) => g.repoUrl).filter(Boolean)).size);
|
|
58
58
|
</script>
|
|
@@ -308,26 +308,26 @@ const sReview = ref<HTMLElement | null>(null);
|
|
|
308
308
|
const { animateCount } = useHelpers();
|
|
309
309
|
|
|
310
310
|
const stuckTasks = computed(() => (s.value.stuckTasks || []) as StuckTask[]);
|
|
311
|
-
const
|
|
311
|
+
const _reviewPrs = computed(() => (s.value.reviewPrs || []) as ReviewPr[]);
|
|
312
312
|
const stuckDiscussionCount = computed(() => stuckTasks.value.filter((t) => t.taskType === "suggestionRevision").length);
|
|
313
313
|
const total = computed(() => s.value.issueCount || 0);
|
|
314
314
|
const pctOf = (n: number) => Math.min(100, (n / Math.max(total.value, 1)) * 100);
|
|
315
315
|
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
const
|
|
316
|
+
const _jiraBarPct = computed(() => pctOf(total.value));
|
|
317
|
+
const _ghBarPct = computed(() => pctOf(s.value.githubIssueCount || 0));
|
|
318
|
+
const _prBarPct = computed(() => pctOf(s.value.draftPrCount || 0));
|
|
319
|
+
const _reviewBarPct = computed(() => pctOf(s.value.reviewPrCount || 0));
|
|
320
|
+
const _discussionBarPct = computed(() =>
|
|
321
321
|
stuckDiscussionCount.value > 0 ? 100 : s.value.lastReviewDiscussionSuccessAt ? 60 : 0
|
|
322
322
|
);
|
|
323
|
-
const
|
|
323
|
+
const _stuckBarPct = computed(() => pctOf(stuckTasks.value.length));
|
|
324
324
|
|
|
325
|
-
const
|
|
325
|
+
const _uptime = computed(() => {
|
|
326
326
|
if (!s.value.startedAt || !status.value?.worker.running) return "Stopped";
|
|
327
327
|
return formatDuration(Date.now() - new Date(s.value.startedAt).getTime());
|
|
328
328
|
});
|
|
329
329
|
|
|
330
|
-
const
|
|
330
|
+
const _overviewSub = computed(() => {
|
|
331
331
|
if (!status.value) return "Loading…";
|
|
332
332
|
if (!s.value.lastPollAt) return "No poll data yet";
|
|
333
333
|
return `Last updated ${formatRelative(s.value.lastPollAt)}`;
|
|
@@ -350,17 +350,17 @@ watch(
|
|
|
350
350
|
// Stuck tasks sort
|
|
351
351
|
const stuckSort = ref<{ col: string; dir: "asc" | "desc" }>({ col: "failedAt", dir: "desc" });
|
|
352
352
|
|
|
353
|
-
function
|
|
353
|
+
function _toggleStuckSort(col: string) {
|
|
354
354
|
stuckSort.value =
|
|
355
355
|
stuckSort.value.col === col ? { col, dir: stuckSort.value.dir === "desc" ? "asc" : "desc" } : { col, dir: "desc" };
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
function
|
|
358
|
+
function _stuckSortIco(col: string): string {
|
|
359
359
|
if (stuckSort.value.col !== col) return "↕";
|
|
360
360
|
return stuckSort.value.dir === "desc" ? "↓" : "↑";
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
-
const
|
|
363
|
+
const _sortedStuckTasks = computed(() => {
|
|
364
364
|
const { col, dir } = stuckSort.value;
|
|
365
365
|
return [...stuckTasks.value].sort((a, b) => {
|
|
366
366
|
const av = (a as Record<string, unknown>)[col] ?? "";
|
|
@@ -373,7 +373,7 @@ const sortedStuckTasks = computed(() => {
|
|
|
373
373
|
// Issues table
|
|
374
374
|
const allIssues = computed(() => ((s.value.allIssues || s.value.issues || []) as WorkflowIssue[]).slice(0, 500));
|
|
375
375
|
|
|
376
|
-
const
|
|
376
|
+
const _availableStates = computed(() => {
|
|
377
377
|
const states = new Set<string>();
|
|
378
378
|
for (const i of allIssues.value) if (i.workflowState) states.add(i.workflowState);
|
|
379
379
|
return [...states].sort();
|
|
@@ -385,14 +385,14 @@ const issueSort = ref<{ col: string; dir: "asc" | "desc" }>({ col: "key", dir: "
|
|
|
385
385
|
const issuePage = ref(1);
|
|
386
386
|
const issuePageSize = 25;
|
|
387
387
|
|
|
388
|
-
function
|
|
388
|
+
function _toggleIssueSort(col: string) {
|
|
389
389
|
issueSort.value =
|
|
390
390
|
issueSort.value.col === col
|
|
391
391
|
? { col, dir: issueSort.value.dir === "desc" ? "asc" : "desc" }
|
|
392
392
|
: { col, dir: col === "key" ? "asc" : "desc" };
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
function
|
|
395
|
+
function _issueSortIco(col: string): string {
|
|
396
396
|
if (issueSort.value.col !== col) return "↕";
|
|
397
397
|
return issueSort.value.dir === "desc" ? "↓" : "↑";
|
|
398
398
|
}
|
|
@@ -413,9 +413,9 @@ const filteredIssues = computed((): WorkflowIssue[] => {
|
|
|
413
413
|
const totalIssuePages = computed(() => Math.max(1, Math.ceil(filteredIssues.value.length / issuePageSize)));
|
|
414
414
|
const issueStart = computed(() => (issuePage.value - 1) * issuePageSize);
|
|
415
415
|
const issueEnd = computed(() => Math.min(issueStart.value + issuePageSize, filteredIssues.value.length));
|
|
416
|
-
const
|
|
416
|
+
const _paginatedIssues = computed(() => filteredIssues.value.slice(issueStart.value, issueEnd.value));
|
|
417
417
|
|
|
418
|
-
const
|
|
418
|
+
const _pageNumbers = computed(() => {
|
|
419
419
|
const total = totalIssuePages.value;
|
|
420
420
|
const cur = issuePage.value;
|
|
421
421
|
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
|
@@ -431,7 +431,7 @@ watch(filteredIssues, () => {
|
|
|
431
431
|
issuePage.value = 1;
|
|
432
432
|
});
|
|
433
433
|
|
|
434
|
-
const
|
|
434
|
+
const _activePollers = computed(() => {
|
|
435
435
|
const busy = s.value.pollerBusy || {};
|
|
436
436
|
const current = s.value.pollerCurrent || {};
|
|
437
437
|
const map: Record<string, string> = {
|
|
@@ -446,21 +446,21 @@ const activePollers = computed(() => {
|
|
|
446
446
|
.map(([k, name]) => ({ name, current: (current as Record<string, string>)[k] || "" }));
|
|
447
447
|
});
|
|
448
448
|
|
|
449
|
-
function
|
|
449
|
+
function _getIssuePr(i: WorkflowIssue): DraftPr | undefined {
|
|
450
450
|
const draftPrs = (status.value?.state?.draftPrs || []) as DraftPr[];
|
|
451
451
|
return draftPrs.find((p) => (i.repoUrls || []).some((r) => p.repoUrl === r));
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
function
|
|
454
|
+
function _openDrawer(issue: WorkflowIssue) {
|
|
455
455
|
drawerIssue.value = issue;
|
|
456
456
|
drawerOpen.value = true;
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
-
function
|
|
459
|
+
function _closeDrawer() {
|
|
460
460
|
drawerOpen.value = false;
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
-
async function
|
|
463
|
+
async function _refresh() {
|
|
464
464
|
await loadStatus();
|
|
465
465
|
}
|
|
466
466
|
|
|
@@ -48,7 +48,7 @@ const TABS = [
|
|
|
48
48
|
type TabId = (typeof TABS)[number]["id"];
|
|
49
49
|
|
|
50
50
|
const activeTab = ref<TabId>("all");
|
|
51
|
-
const
|
|
51
|
+
const _autoScroll = ref(true);
|
|
52
52
|
const connecting = ref(false);
|
|
53
53
|
const logLines = ref<string[]>([]);
|
|
54
54
|
let src: EventSource | null = null;
|
|
@@ -70,7 +70,7 @@ function getTag(line: string): string | null {
|
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const
|
|
73
|
+
const _filteredLines = computed(() => {
|
|
74
74
|
const tab = TABS.find((t) => t.id === activeTab.value);
|
|
75
75
|
if (!tab) return logLines.value;
|
|
76
76
|
if (tab.tag === null) return logLines.value;
|
|
@@ -78,7 +78,7 @@ const filteredLines = computed(() => {
|
|
|
78
78
|
return logLines.value.filter((l) => getTag(l) === tab.tag);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
const
|
|
81
|
+
const _counts = computed(() => {
|
|
82
82
|
const result: Record<string, number> = {};
|
|
83
83
|
for (const tab of TABS) {
|
|
84
84
|
if (tab.tag === null) continue;
|
|
@@ -116,7 +116,7 @@ function expandAll(node: MemNode) {
|
|
|
116
116
|
|
|
117
117
|
const tree = computed(() => buildTree(memFiles.value));
|
|
118
118
|
|
|
119
|
-
const
|
|
119
|
+
const _rootNodes = computed(() => {
|
|
120
120
|
const children = [...tree.value.children.values()].sort((a, b) => {
|
|
121
121
|
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
122
122
|
return a.name.localeCompare(b.name);
|
|
@@ -137,7 +137,7 @@ async function loadMemoryList() {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
function
|
|
140
|
+
function _toggleDir(path: string) {
|
|
141
141
|
if (expanded.value.has(path)) expanded.value.delete(path);
|
|
142
142
|
else expanded.value.add(path);
|
|
143
143
|
currentPath.value = path;
|
|
@@ -145,7 +145,7 @@ function toggleDir(path: string) {
|
|
|
145
145
|
fileReadonly.value = true;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
async function
|
|
148
|
+
async function _selectFile(path: string) {
|
|
149
149
|
currentPath.value = path;
|
|
150
150
|
isDir.value = false;
|
|
151
151
|
fileContent.value = "Loading…";
|
|
@@ -169,7 +169,7 @@ async function selectFile(path: string) {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
async function
|
|
172
|
+
async function _saveMemory() {
|
|
173
173
|
if (!currentPath.value) return;
|
|
174
174
|
try {
|
|
175
175
|
await apiPut(`/api/memory/${encodeURIComponent(currentPath.value)}`, { content: fileContent.value });
|
|
@@ -121,7 +121,7 @@ const STAGE_PCT: Record<string, number> = {
|
|
|
121
121
|
done: 100
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
-
const
|
|
124
|
+
const _boards = computed(() => {
|
|
125
125
|
const issues = status.value?.state?.issues || [];
|
|
126
126
|
return [...new Set(issues.map((i) => i.projectKey).filter(Boolean))].sort() as string[];
|
|
127
127
|
});
|
|
@@ -143,17 +143,17 @@ const groupedIssues = computed(() => {
|
|
|
143
143
|
return grouped;
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
function
|
|
146
|
+
function _ghIssueState(url: string): string | undefined {
|
|
147
147
|
const ghIssues = status.value?.state?.githubIssues || [];
|
|
148
148
|
return ghIssues.find((g) => g.issueUrl === url)?.state;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
function
|
|
151
|
+
function _mergedPrs(c: WorkflowIssue): DraftPr[] {
|
|
152
152
|
const draftPrs = (status.value?.state?.draftPrs || []) as DraftPr[];
|
|
153
153
|
return draftPrs.filter((p) => (c.repoUrls || []).includes(p.repoUrl) && p.status === "merged");
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
function
|
|
156
|
+
function _openDrawer(issue: WorkflowIssue) {
|
|
157
157
|
drawerIssue.value = issue;
|
|
158
158
|
drawerOpen.value = true;
|
|
159
159
|
}
|
|
@@ -73,7 +73,7 @@ interface RepoEntry {
|
|
|
73
73
|
prs: DraftPr[];
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const
|
|
76
|
+
const _repoList = computed((): RepoEntry[] => {
|
|
77
77
|
if (!status.value) return [];
|
|
78
78
|
const issues = (status.value.state?.issues || []) as WorkflowIssue[];
|
|
79
79
|
const draftPrs = (status.value.state?.draftPrs || []) as DraftPr[];
|
|
@@ -322,25 +322,25 @@ const filterModel = ref("");
|
|
|
322
322
|
const allSessions = computed(() => rawData.value?.sessions || []);
|
|
323
323
|
const issueNames = computed(() => rawData.value?.issueNames || {});
|
|
324
324
|
|
|
325
|
-
const
|
|
325
|
+
const _availableYears = computed(() => {
|
|
326
326
|
const ys = new Set<string>();
|
|
327
327
|
for (const s of allSessions.value) ys.add(s.timestamp.slice(0, 4));
|
|
328
328
|
return [...ys].sort((a, b) => b.localeCompare(a));
|
|
329
329
|
});
|
|
330
330
|
|
|
331
|
-
const
|
|
331
|
+
const _availableProjects = computed(() => {
|
|
332
332
|
const ps = new Set<string>();
|
|
333
333
|
for (const s of allSessions.value) if (s.project) ps.add(s.project);
|
|
334
334
|
return [...ps].sort();
|
|
335
335
|
});
|
|
336
336
|
|
|
337
|
-
const
|
|
337
|
+
const _availableModels = computed(() => {
|
|
338
338
|
const ms = new Set<string>();
|
|
339
339
|
for (const s of allSessions.value) if (s.model) ms.add(s.model);
|
|
340
340
|
return [...ms].sort();
|
|
341
341
|
});
|
|
342
342
|
|
|
343
|
-
const
|
|
343
|
+
const _isFiltered = computed(
|
|
344
344
|
() => !!(filterKw.value || filterYear.value || filterMonth.value || filterProject.value || filterModel.value)
|
|
345
345
|
);
|
|
346
346
|
|
|
@@ -362,12 +362,12 @@ const filteredSessions = computed(() => {
|
|
|
362
362
|
|
|
363
363
|
// Aggregates
|
|
364
364
|
const hasTokens = computed(() => filteredSessions.value.some((s) => s.inputTokens > 0 || s.outputTokens > 0));
|
|
365
|
-
const
|
|
366
|
-
const
|
|
365
|
+
const _totalCost = computed(() => filteredSessions.value.reduce((a, s) => a + s.cost, 0));
|
|
366
|
+
const _totalDur = computed(() => filteredSessions.value.reduce((a, s) => a + s.duration, 0));
|
|
367
367
|
const totInput = computed(() => filteredSessions.value.reduce((a, s) => a + s.inputTokens, 0));
|
|
368
368
|
const totOutput = computed(() => filteredSessions.value.reduce((a, s) => a + s.outputTokens, 0));
|
|
369
|
-
const
|
|
370
|
-
const
|
|
369
|
+
const _totCacheR = computed(() => filteredSessions.value.reduce((a, s) => a + s.cacheReadTokens, 0));
|
|
370
|
+
const _totCacheC = computed(() => filteredSessions.value.reduce((a, s) => a + s.cacheCreationTokens, 0));
|
|
371
371
|
|
|
372
372
|
// By Project
|
|
373
373
|
interface ProjData {
|
|
@@ -392,7 +392,7 @@ const byProject = computed((): [string, ProjData][] => {
|
|
|
392
392
|
|
|
393
393
|
const maxProjCost = computed(() => byProject.value[0]?.[1].cost || 1);
|
|
394
394
|
|
|
395
|
-
function
|
|
395
|
+
function _projBarWidth(d: ProjData, field: "input" | "output" | "cacheRead"): number {
|
|
396
396
|
const total = totInput.value + totOutput.value;
|
|
397
397
|
if (!total || !hasTokens.value) return 0;
|
|
398
398
|
return (d[field] / total) * 100 * (d.cost / maxProjCost.value);
|
|
@@ -421,10 +421,10 @@ const byDay = computed((): [string, DayData][] => {
|
|
|
421
421
|
});
|
|
422
422
|
|
|
423
423
|
// Oldest→newest for the bar chart display
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
const
|
|
424
|
+
const _dayChartData = computed(() => [...byDay.value].reverse());
|
|
425
|
+
const _byDayMap = computed(() => Object.fromEntries(byDay.value));
|
|
426
|
+
const _maxDayCost = computed(() => Math.max(...byDay.value.map(([, d]) => d.cost), 0.00001));
|
|
427
|
+
const _hoveredDay = ref<string | null>(null);
|
|
428
428
|
|
|
429
429
|
// By Model
|
|
430
430
|
interface ModelData {
|
|
@@ -451,7 +451,7 @@ const byModel = computed((): [string, ModelData][] => {
|
|
|
451
451
|
});
|
|
452
452
|
|
|
453
453
|
// Donut chart
|
|
454
|
-
const
|
|
454
|
+
const _modelSlices = computed(() => {
|
|
455
455
|
const total = byModel.value.reduce((a, [, d]) => a + d.cost, 0) || 1;
|
|
456
456
|
let start = 0;
|
|
457
457
|
return byModel.value.map(([model, d]) => {
|
|
@@ -462,7 +462,7 @@ const modelSlices = computed(() => {
|
|
|
462
462
|
});
|
|
463
463
|
});
|
|
464
464
|
|
|
465
|
-
function
|
|
465
|
+
function _donutArc(cx: number, cy: number, outerR: number, innerR: number, startDeg: number, endDeg: number): string {
|
|
466
466
|
if (endDeg - startDeg >= 360) endDeg = startDeg + 359.99;
|
|
467
467
|
const toRad = (deg: number) => ((deg - 90) * Math.PI) / 180;
|
|
468
468
|
const cos = Math.cos,
|
|
@@ -552,9 +552,9 @@ const filteredIssues = computed((): [string, IssueData][] => {
|
|
|
552
552
|
const totalIssuePages = computed(() => Math.max(1, Math.ceil(filteredIssues.value.length / issuePageSize)));
|
|
553
553
|
const issueStart = computed(() => (issuePage.value - 1) * issuePageSize);
|
|
554
554
|
const issueEnd = computed(() => Math.min(issueStart.value + issuePageSize, filteredIssues.value.length));
|
|
555
|
-
const
|
|
555
|
+
const _paginatedIssues = computed(() => filteredIssues.value.slice(issueStart.value, issueEnd.value));
|
|
556
556
|
|
|
557
|
-
const
|
|
557
|
+
const _pageNumbers = computed(() => {
|
|
558
558
|
const total = totalIssuePages.value;
|
|
559
559
|
const cur = issuePage.value;
|
|
560
560
|
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
|
@@ -570,7 +570,7 @@ watch(filteredIssues, () => {
|
|
|
570
570
|
issuePage.value = 1;
|
|
571
571
|
});
|
|
572
572
|
|
|
573
|
-
function
|
|
573
|
+
function _toggleSort(col: string) {
|
|
574
574
|
if (issueSort.value.col === col) {
|
|
575
575
|
issueSort.value = { col, dir: issueSort.value.dir === "desc" ? "asc" : "desc" };
|
|
576
576
|
} else {
|
|
@@ -578,22 +578,22 @@ function toggleSort(col: string) {
|
|
|
578
578
|
}
|
|
579
579
|
}
|
|
580
580
|
|
|
581
|
-
function
|
|
581
|
+
function _sortIco(col: string): string {
|
|
582
582
|
if (issueSort.value.col !== col) return "↕";
|
|
583
583
|
return issueSort.value.dir === "desc" ? "↓" : "↑";
|
|
584
584
|
}
|
|
585
585
|
|
|
586
586
|
// Navigation
|
|
587
|
-
function
|
|
587
|
+
function _openJira(key: string) {
|
|
588
588
|
if (!jiraBaseUrl.value) return;
|
|
589
589
|
window.open(`${jiraBaseUrl.value}/browse/${key}`, "_blank", "noreferrer");
|
|
590
590
|
}
|
|
591
591
|
|
|
592
592
|
// Helpers
|
|
593
|
-
function
|
|
593
|
+
function _shortModel(m: string): string {
|
|
594
594
|
return m.replace("claude-", "").replace(/-\d{8}$/, "");
|
|
595
595
|
}
|
|
596
|
-
function
|
|
596
|
+
function _monthName(m: number): string {
|
|
597
597
|
return new Date(2000, m - 1, 1).toLocaleString("en-US", { month: "long" });
|
|
598
598
|
}
|
|
599
599
|
function modelColor(model: string): string {
|
|
@@ -602,13 +602,13 @@ function modelColor(model: string): string {
|
|
|
602
602
|
if (model.includes("haiku")) return "var(--teal)";
|
|
603
603
|
return "var(--sky)";
|
|
604
604
|
}
|
|
605
|
-
function
|
|
605
|
+
function _fmtCost(v: number): string {
|
|
606
606
|
return v > 0 ? `$${v.toFixed(4)}` : "$0.0000";
|
|
607
607
|
}
|
|
608
|
-
function
|
|
608
|
+
function _fmtDur(ms: number): string {
|
|
609
609
|
return ms >= 60000 ? `${(ms / 60000).toFixed(1)}m` : `${(ms / 1000).toFixed(1)}s`;
|
|
610
610
|
}
|
|
611
|
-
function
|
|
611
|
+
function _fmtTok(n: number): string {
|
|
612
612
|
return n >= 1e6 ? `${(n / 1e6).toFixed(2)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
|
|
613
613
|
}
|
|
614
614
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isClaudeCliError, runClaudeGitHubIssueReview } from "../claude.js";
|
|
1
|
+
import { isClaudeCliError, runClaudeBranchSlug, runClaudeGitHubIssueReview } from "../claude.js";
|
|
2
2
|
import { deriveEpicMemoryPath, saveIssueMemory } from "../memory.js";
|
|
3
3
|
import { ensureBranchFromDefault, ensureWorktreeForBranch, pullLatest, removeWorktree } from "../repo.js";
|
|
4
4
|
import {
|
|
@@ -207,7 +207,9 @@ export async function processGitHubIssue({
|
|
|
207
207
|
const approvalComment = isApprovalComment(latestComment.body);
|
|
208
208
|
|
|
209
209
|
if (approvalComment) {
|
|
210
|
-
const
|
|
210
|
+
const englishSlug = githubIssueMemory.branch_name ? "" : await runClaudeBranchSlug(detail.title);
|
|
211
|
+
const nextBranchName =
|
|
212
|
+
githubIssueMemory.branch_name || buildImplementationBranchName(detail.title, detail.number, englishSlug);
|
|
211
213
|
const existingPr = await github.findPullRequestByHead(githubIssueMemory.repo_url, nextBranchName);
|
|
212
214
|
if (!existingPr) {
|
|
213
215
|
githubIssueMemory.pr_url = "";
|
|
@@ -301,16 +301,35 @@ export function isPullRequestReviewRequestComment(body, githubBotUser) {
|
|
|
301
301
|
return normalizedBody.includes(`requested review from @${botLogin}`);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
export function buildImplementationBranchName(title, issueNumber) {
|
|
304
|
+
export function buildImplementationBranchName(title, issueNumber, englishSlug = "") {
|
|
305
305
|
const jiraKey = extractJiraKey(title);
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
.slice(0, 48);
|
|
306
|
+
const type = detectBranchType(title);
|
|
307
|
+
const id = (jiraKey || `gh-${issueNumber}`).toLowerCase();
|
|
308
|
+
const suffix = englishSlug ? `-${englishSlug}` : "";
|
|
309
|
+
return `${type}/${id}${suffix}`;
|
|
310
|
+
}
|
|
312
311
|
|
|
313
|
-
|
|
312
|
+
function detectBranchType(title) {
|
|
313
|
+
const t = String(title || "");
|
|
314
|
+
|
|
315
|
+
const prefixMatch =
|
|
316
|
+
/^\s*(?:\[[A-Z][A-Z0-9]+-\d+\]\s*)?(fix|feat|refactor|perf|docs|ci|build|revert|chore|vendor|security|other)\s*:/iu.exec(
|
|
317
|
+
t
|
|
318
|
+
);
|
|
319
|
+
if (prefixMatch) return prefixMatch[1].toLowerCase();
|
|
320
|
+
|
|
321
|
+
const lower = t.toLowerCase();
|
|
322
|
+
if (/\b(fix|hotfix|patch)\b|sửa lỗi|bug fix/.test(lower)) return "fix";
|
|
323
|
+
if (/\brefactor\b|tái cấu trúc/.test(lower)) return "refactor";
|
|
324
|
+
if (/\b(perf|performance)\b|hiệu năng|hiệu suất|tối ưu/.test(lower)) return "perf";
|
|
325
|
+
if (/\b(docs?|document|readme)\b|tài liệu/.test(lower)) return "docs";
|
|
326
|
+
if (/\bci\b|pipeline/.test(lower)) return "ci";
|
|
327
|
+
if (/\bbuild\b/.test(lower)) return "build";
|
|
328
|
+
if (/\b(revert|rollback)\b|hoàn tác/.test(lower)) return "revert";
|
|
329
|
+
if (/\bchore\b|cấu hình/.test(lower)) return "chore";
|
|
330
|
+
if (/\b(vendor|dependency|npm|yarn)\b|thư viện/.test(lower)) return "vendor";
|
|
331
|
+
if (/\b(security|sonarqube)\b|bảo mật/.test(lower)) return "security";
|
|
332
|
+
return "feat";
|
|
314
333
|
}
|
|
315
334
|
|
|
316
335
|
export async function buildPullRequestRepoAccessPlan({ repo, pullRequestBody, latestCommentBody = "", epicMemory }) {
|