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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-teammate",
3
- "version": "0.1.261",
3
+ "version": "0.1.263",
4
4
  "description": "CLI bootstrapper for Claude Teammate.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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 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);
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 getIssue(key: string | undefined) {
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 itemLabel(item: {
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 filters = [
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 stageColor = computed(() => STAGE_COLORS[props.issue?.workflowState || ""] || "var(--sky)");
288
+ const _stageColor = computed(() => STAGE_COLORS[props.issue?.workflowState || ""] || "var(--sky)");
289
289
  const progressWidth = ref(0);
290
290
 
291
- const draftPrs = computed((): DraftPr[] => {
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 trackedGhIssues = computed((): GithubIssue[] => {
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 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));
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 isEntryVisible(type: string): boolean {
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 setLogFilter(f: string) {
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 refreshLog() {
491
+ function _refreshLog() {
492
492
  if (props.issue?.key) connectLogStream(props.issue.key);
493
493
  }
494
494
 
495
- function downloadLog() {
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 resetIssue() {
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 renderLine(line: string): string {
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 fmtBytes = inject<(b: number) => string>("fmtBytes", (b) => String(b));
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 icon = computed(() => {
59
+ const _icon = computed(() => {
60
60
  if (props.node.isDir) return isExpanded.value ? "▾" : "▸";
61
61
  return "·";
62
62
  });
63
63
 
64
- const nodeClass = computed(() => {
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 sortedChildren = computed(() => {
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 handleClick() {
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 toggleSidebar() {
161
+ function _toggleSidebar() {
162
162
  sidebarOpen.value = !sidebarOpen.value;
163
163
  }
164
164
 
165
- function closeSidebar() {
165
+ function _closeSidebar() {
166
166
  sidebarOpen.value = false;
167
167
  }
168
168
 
169
- const workerClass = computed(() => {
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 workerText = computed(() => {
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 pipelineCount = computed(() => {
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 awaitingCount = computed(() => {
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 events = computed((): ActivityEvent[] => {
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 boards = computed(() => {
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 total = computed(
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 configEntries = computed(() => Object.entries(config.value));
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 repoCount = computed(() => new Set(githubIssues.value.map((g) => g.repoUrl).filter(Boolean)).size);
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 reviewPrs = computed(() => (s.value.reviewPrs || []) as ReviewPr[]);
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 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(() =>
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 stuckBarPct = computed(() => pctOf(stuckTasks.value.length));
323
+ const _stuckBarPct = computed(() => pctOf(stuckTasks.value.length));
324
324
 
325
- const uptime = computed(() => {
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 overviewSub = computed(() => {
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 toggleStuckSort(col: string) {
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 stuckSortIco(col: string): string {
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 sortedStuckTasks = computed(() => {
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 availableStates = computed(() => {
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 toggleIssueSort(col: string) {
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 issueSortIco(col: string): string {
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 paginatedIssues = computed(() => filteredIssues.value.slice(issueStart.value, issueEnd.value));
416
+ const _paginatedIssues = computed(() => filteredIssues.value.slice(issueStart.value, issueEnd.value));
417
417
 
418
- const pageNumbers = computed(() => {
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 activePollers = computed(() => {
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 getIssuePr(i: WorkflowIssue): DraftPr | undefined {
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 openDrawer(issue: WorkflowIssue) {
454
+ function _openDrawer(issue: WorkflowIssue) {
455
455
  drawerIssue.value = issue;
456
456
  drawerOpen.value = true;
457
457
  }
458
458
 
459
- function closeDrawer() {
459
+ function _closeDrawer() {
460
460
  drawerOpen.value = false;
461
461
  }
462
462
 
463
- async function refresh() {
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 autoScroll = ref(true);
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 filteredLines = computed(() => {
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 counts = computed(() => {
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 rootNodes = computed(() => {
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 toggleDir(path: string) {
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 selectFile(path: string) {
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 saveMemory() {
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 boards = computed(() => {
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 ghIssueState(url: string): string | undefined {
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 mergedPrs(c: WorkflowIssue): DraftPr[] {
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 openDrawer(issue: WorkflowIssue) {
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 repoList = computed((): RepoEntry[] => {
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 availableYears = computed(() => {
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 availableProjects = computed(() => {
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 availableModels = computed(() => {
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 isFiltered = computed(
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 totalCost = computed(() => filteredSessions.value.reduce((a, s) => a + s.cost, 0));
366
- const totalDur = computed(() => filteredSessions.value.reduce((a, s) => a + s.duration, 0));
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 totCacheR = computed(() => filteredSessions.value.reduce((a, s) => a + s.cacheReadTokens, 0));
370
- const totCacheC = computed(() => filteredSessions.value.reduce((a, s) => a + s.cacheCreationTokens, 0));
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 projBarWidth(d: ProjData, field: "input" | "output" | "cacheRead"): number {
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 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);
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 modelSlices = computed(() => {
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 donutArc(cx: number, cy: number, outerR: number, innerR: number, startDeg: number, endDeg: number): string {
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 paginatedIssues = computed(() => filteredIssues.value.slice(issueStart.value, issueEnd.value));
555
+ const _paginatedIssues = computed(() => filteredIssues.value.slice(issueStart.value, issueEnd.value));
556
556
 
557
- const pageNumbers = computed(() => {
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 toggleSort(col: string) {
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 sortIco(col: string): string {
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 openJira(key: string) {
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 shortModel(m: string): string {
593
+ function _shortModel(m: string): string {
594
594
  return m.replace("claude-", "").replace(/-\d{8}$/, "");
595
595
  }
596
- function monthName(m: number): string {
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 fmtCost(v: number): string {
605
+ function _fmtCost(v: number): string {
606
606
  return v > 0 ? `$${v.toFixed(4)}` : "$0.0000";
607
607
  }
608
- function fmtDur(ms: number): string {
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 fmtTok(n: number): string {
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 nextBranchName = githubIssueMemory.branch_name || buildImplementationBranchName(detail.title, detail.number);
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 slug = normalizePlanTitleText(title)
307
- .toLowerCase()
308
- .replace(/\[[^\]]+\]/gu, "")
309
- .replace(/[^a-z0-9]+/gu, "-")
310
- .replace(/^-+|-+$/gu, "")
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
- return `${(jiraKey || `gh-${issueNumber}`).toLowerCase()}-${slug || "plan"}`;
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 }) {