@akiojin/gwt 2.6.1 → 2.8.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 (55) hide show
  1. package/README.ja.md +1 -1
  2. package/README.md +2 -2
  3. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  4. package/dist/cli/ui/components/screens/BranchListScreen.js +5 -1
  5. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  6. package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
  7. package/dist/cli/ui/hooks/useGitData.js +27 -7
  8. package/dist/cli/ui/hooks/useGitData.js.map +1 -1
  9. package/dist/cli/ui/types.d.ts +4 -0
  10. package/dist/cli/ui/types.d.ts.map +1 -1
  11. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  12. package/dist/cli/ui/utils/branchFormatter.js +47 -0
  13. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  14. package/dist/client/assets/{index-DxHGLTNq.js → index-CNWntAlF.js} +15 -15
  15. package/dist/client/index.html +1 -1
  16. package/dist/config/index.d.ts +17 -0
  17. package/dist/config/index.d.ts.map +1 -1
  18. package/dist/config/index.js +66 -1
  19. package/dist/config/index.js.map +1 -1
  20. package/dist/git.d.ts.map +1 -1
  21. package/dist/git.js +114 -30
  22. package/dist/git.js.map +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +3 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/types/api.d.ts +11 -0
  27. package/dist/types/api.d.ts.map +1 -1
  28. package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
  29. package/dist/web/client/src/pages/BranchDetailPage.js +55 -0
  30. package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
  31. package/dist/web/server/routes/sessions.d.ts.map +1 -1
  32. package/dist/web/server/routes/sessions.js +35 -0
  33. package/dist/web/server/routes/sessions.js.map +1 -1
  34. package/dist/web/server/services/branches.d.ts.map +1 -1
  35. package/dist/web/server/services/branches.js +13 -2
  36. package/dist/web/server/services/branches.js.map +1 -1
  37. package/dist/worktree.d.ts.map +1 -1
  38. package/dist/worktree.js +3 -1
  39. package/dist/worktree.js.map +1 -1
  40. package/package.json +1 -8
  41. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +53 -0
  42. package/src/cli/ui/__tests__/integration/navigation.test.tsx +5 -0
  43. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +37 -0
  44. package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -1
  45. package/src/cli/ui/hooks/useGitData.ts +27 -7
  46. package/src/cli/ui/types.ts +5 -0
  47. package/src/cli/ui/utils/branchFormatter.ts +47 -0
  48. package/src/config/index.ts +87 -1
  49. package/src/git.ts +114 -30
  50. package/src/index.ts +3 -0
  51. package/src/types/api.ts +12 -0
  52. package/src/web/client/src/pages/BranchDetailPage.tsx +69 -1
  53. package/src/web/server/routes/sessions.ts +39 -0
  54. package/src/web/server/services/branches.ts +18 -2
  55. package/src/worktree.ts +7 -1
@@ -10,7 +10,11 @@ import {
10
10
  import { useConfig } from "../hooks/useConfig";
11
11
  import { ApiError } from "../lib/api";
12
12
  import { Terminal } from "../components/Terminal";
13
- import type { Branch, CustomAITool } from "../../../../types/api.js";
13
+ import type {
14
+ Branch,
15
+ CustomAITool,
16
+ LastToolUsage,
17
+ } from "../../../../types/api.js";
14
18
 
15
19
  type ToolType = "claude-code" | "codex-cli" | "custom";
16
20
  type ToolMode = "normal" | "continue" | "resume";
@@ -410,6 +414,29 @@ export function BranchDetailPage() {
410
414
  .sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
411
415
  }, [sessionsData, branch?.worktreePath]);
412
416
 
417
+ const latestToolUsage: LastToolUsage | null = useMemo(() => {
418
+ if (branch?.lastToolUsage) {
419
+ return branch.lastToolUsage;
420
+ }
421
+ const first = branchSessions[0];
422
+ if (!first) return null;
423
+ return {
424
+ branch: branch.name,
425
+ worktreePath: branch.worktreePath ?? null,
426
+ toolId:
427
+ first.toolType === "custom"
428
+ ? first.toolName ?? "custom"
429
+ : (first.toolType as LastToolUsage["toolId"]),
430
+ toolLabel:
431
+ first.toolType === "custom"
432
+ ? first.toolName ?? "Custom"
433
+ : toolLabel(first.toolType),
434
+ mode: first.mode ?? "normal",
435
+ model: null,
436
+ timestamp: first.startedAt ? Date.parse(first.startedAt) : Date.now(),
437
+ };
438
+ }, [branch?.lastToolUsage, branch?.name, branch?.worktreePath, branchSessions]);
439
+
413
440
  const handleSessionExit = (code: number) => {
414
441
  setActiveSessionId(null);
415
442
  setIsTerminalFullscreen(false);
@@ -449,6 +476,21 @@ export function BranchDetailPage() {
449
476
  {branch.worktreePath ? "Worktreeあり" : "Worktree未作成"}
450
477
  </span>
451
478
  </div>
479
+ <div className="badge-group" style={{ marginTop: "0.5rem" }}>
480
+ {latestToolUsage ? (
481
+ <>
482
+ <span className="status-badge status-badge--muted">
483
+ {renderToolUsage(latestToolUsage)}
484
+ </span>
485
+ <span className="status-badge status-badge--muted">
486
+ {formatUsageTimestamp(latestToolUsage.timestamp)} ・ worktree:{" "}
487
+ {latestToolUsage.worktreePath ?? branch.worktreePath ?? "N/A"}
488
+ </span>
489
+ </>
490
+ ) : (
491
+ <span className="status-badge status-badge--muted">Unknown</span>
492
+ )}
493
+ </div>
452
494
  <div className="page-hero__actions">
453
495
  {!canStartSession ? (
454
496
  <button
@@ -950,6 +992,32 @@ const SESSION_STATUS_LABEL: Record<
950
992
  failed: "failed",
951
993
  };
952
994
 
995
+ function renderToolUsage(usage: LastToolUsage): string {
996
+ const modeLabel =
997
+ usage.mode === "normal"
998
+ ? "New"
999
+ : usage.mode === "continue"
1000
+ ? "Continue"
1001
+ : usage.mode === "resume"
1002
+ ? "Resume"
1003
+ : null;
1004
+ const toolText = mapToolLabel(usage.toolId, usage.toolLabel);
1005
+ return [toolText, modeLabel, usage.model].filter(Boolean).join(" | ");
1006
+ }
1007
+
1008
+ function formatUsageTimestamp(value: number): string {
1009
+ return formatDate(new Date(value).toISOString());
1010
+ }
1011
+
1012
+ function mapToolLabel(toolId: string, toolLabel?: string | null): string {
1013
+ if (toolId === "claude-code") return "Claude";
1014
+ if (toolId === "codex-cli") return "Codex";
1015
+ if (toolId === "gemini-cli") return "Gemini";
1016
+ if (toolId === "qwen-cli") return "Qwen";
1017
+ if (toolLabel) return toolLabel;
1018
+ return "Custom";
1019
+ }
1020
+
953
1021
  function parseExtraArgs(value: string): string[] {
954
1022
  return value
955
1023
  .split(/\s+/)
@@ -11,6 +11,8 @@ import type {
11
11
  AIToolSession,
12
12
  StartSessionRequest,
13
13
  } from "../../../types/api.js";
14
+ import { saveSession } from "../../../config/index.js";
15
+ import { execa } from "execa";
14
16
 
15
17
  /**
16
18
  * セッション関連のルートを登録
@@ -53,6 +55,37 @@ export async function registerSessionRoutes(
53
55
  toolName,
54
56
  );
55
57
 
58
+ // 履歴を永続化(best-effort)
59
+ try {
60
+ const { stdout: repoRoot } = await execa("git", ["rev-parse", "--show-toplevel"], {
61
+ cwd: worktreePath,
62
+ });
63
+ let branchName: string | null = null;
64
+ try {
65
+ const { stdout: branchStdout } = await execa(
66
+ "git",
67
+ ["rev-parse", "--abbrev-ref", "HEAD"],
68
+ { cwd: worktreePath },
69
+ );
70
+ branchName = branchStdout.trim() || null;
71
+ } catch {
72
+ branchName = null;
73
+ }
74
+
75
+ await saveSession({
76
+ lastWorktreePath: worktreePath,
77
+ lastBranch: branchName,
78
+ lastUsedTool: toolType === "custom" ? toolName ?? "custom" : toolType,
79
+ toolLabel:
80
+ toolType === "custom" ? toolName ?? "Custom" : toolLabelFromType(toolType),
81
+ mode,
82
+ timestamp: Date.now(),
83
+ repositoryRoot: repoRoot.trim(),
84
+ });
85
+ } catch {
86
+ // ignore persistence errors
87
+ }
88
+
56
89
  reply.code(201);
57
90
  return { success: true, data: session };
58
91
  } catch (error) {
@@ -128,3 +161,9 @@ export async function registerSessionRoutes(
128
161
  }
129
162
  });
130
163
  }
164
+
165
+ function toolLabelFromType(toolType: "claude-code" | "codex-cli" | "custom") {
166
+ if (toolType === "claude-code") return "Claude";
167
+ if (toolType === "codex-cli") return "Codex";
168
+ return "Custom";
169
+ }
@@ -16,6 +16,7 @@ import {
16
16
  import { getPullRequestByBranch } from "../../../github.js";
17
17
  import { listAdditionalWorktrees } from "../../../worktree.js";
18
18
  import type { Branch, BranchSyncResult } from "../../../types/api.js";
19
+ import { getLastToolUsageMap } from "../../../config/index.js";
19
20
 
20
21
  type DivergenceStatus = { remoteAhead: number; localAhead: number };
21
22
  type DivergenceValue = NonNullable<NonNullable<Branch["divergence"]>>;
@@ -37,10 +38,22 @@ function mapPullRequestState(state: string): "open" | "merged" | "closed" {
37
38
  * すべてのブランチ一覧を取得(マージステータスとWorktree情報付き)
38
39
  */
39
40
  export async function listBranches(): Promise<Branch[]> {
40
- const [branches, worktrees, repoRoot] = await Promise.all([
41
+ const repoRoot = await getRepositoryRoot();
42
+ const lastToolUsageMap = await getLastToolUsageMap(repoRoot);
43
+
44
+ // リモートブランチの最新情報を取得(失敗してもローカル情報にはフォールバック)
45
+ try {
46
+ await fetchAllRemotes({ cwd: repoRoot });
47
+ } catch (error) {
48
+ console.warn(
49
+ "Failed to fetch remote branches for Web UI; falling back to local branches",
50
+ error,
51
+ );
52
+ }
53
+
54
+ const [branches, worktrees] = await Promise.all([
41
55
  getAllBranches(),
42
56
  listAdditionalWorktrees(),
43
- getRepositoryRoot(),
44
57
  ]);
45
58
  const divergenceMap = await buildDivergenceMap(branches, repoRoot);
46
59
  const upstreamMap = await collectUpstreamMap(repoRoot);
@@ -92,6 +105,8 @@ export async function listBranches(): Promise<Branch[]> {
92
105
  ? mapDivergence(divergenceStatus)
93
106
  : null;
94
107
 
108
+ const lastToolUsage = lastToolUsageMap.get(branchInfo.name) ?? null;
109
+
95
110
  return {
96
111
  name: branchInfo.name,
97
112
  type: branchInfo.type,
@@ -105,6 +120,7 @@ export async function listBranches(): Promise<Branch[]> {
105
120
  baseBranch: baseBranch ?? null,
106
121
  divergence,
107
122
  prInfo,
123
+ ...(lastToolUsage ? { lastToolUsage } : {}),
108
124
  };
109
125
  }),
110
126
  );
package/src/worktree.ts CHANGED
@@ -98,7 +98,13 @@ export interface WorktreeInfo {
98
98
 
99
99
  async function listWorktrees(): Promise<WorktreeInfo[]> {
100
100
  try {
101
- const { stdout } = await execa("git", ["worktree", "list", "--porcelain"]);
101
+ const { getRepositoryRoot } = await import("./git.js");
102
+ const repoRoot = await getRepositoryRoot();
103
+ const { stdout } = await execa(
104
+ "git",
105
+ ["worktree", "list", "--porcelain"],
106
+ { cwd: repoRoot },
107
+ );
102
108
  const worktrees: WorktreeInfo[] = [];
103
109
  const lines = stdout.split("\n");
104
110