@dyyz1993/create-agent 2.0.0 → 2.1.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 (165) hide show
  1. package/package.json +1 -1
  2. package/src/commands/create.ts +31 -31
  3. package/src/commands/workspace.ts +112 -105
  4. package/src/lib/copy.ts +24 -1
  5. package/templates/agent/electron/main.js +46 -0
  6. package/templates/agent/electron/preload.js +5 -0
  7. package/templates/agent/electron-builder.json +40 -0
  8. package/templates/agent/eslint.config.mjs +17 -2
  9. package/templates/agent/package.json +69 -3
  10. package/templates/agent/src/__tests__/hybrid-mode.test.ts +126 -0
  11. package/templates/agent/src/__tests__/server-config-security.test.ts +55 -0
  12. package/templates/agent/src/bun/index.ts +96 -41
  13. package/templates/agent/src/bun/three.d.ts +1 -0
  14. package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +118 -0
  15. package/templates/agent/src/gateway/http-routes.ts +1 -1
  16. package/templates/agent/src/gateway/ws-handler.ts +84 -56
  17. package/templates/agent/src/mainview/App.tsx +29 -26
  18. package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  19. package/templates/agent/src/mainview/__tests__/setup.ts +32 -29
  20. package/templates/agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
  21. package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
  22. package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +1 -58
  23. package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  24. package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +1 -0
  25. package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +232 -210
  26. package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
  27. package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
  28. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +149 -122
  29. package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
  30. package/templates/agent/src/mainview/lib/api-client.ts +179 -172
  31. package/templates/agent/src/mainview/main.tsx +4 -10
  32. package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
  33. package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
  34. package/templates/agent/src/server-config.ts +34 -32
  35. package/templates/agent/src/server.ts +42 -54
  36. package/templates/agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
  37. package/templates/agent/src/shared/handlers/__tests__/handler-isolation.test.ts +244 -0
  38. package/templates/agent/src/shared/handlers/bash.ts +65 -65
  39. package/templates/agent/src/shared/handlers/chat.ts +189 -155
  40. package/templates/agent/src/shared/handlers/debug.ts +12 -0
  41. package/templates/agent/src/shared/handlers/feed.ts +31 -32
  42. package/templates/agent/src/shared/handlers/git.ts +286 -243
  43. package/templates/agent/src/shared/handlers/index.ts +1 -0
  44. package/templates/agent/src/shared/handlers/rules.ts +31 -31
  45. package/templates/agent/src/shared/handlers/todo.ts +31 -31
  46. package/templates/agent/src/shared/http-routes.ts +250 -0
  47. package/templates/agent/src/shared/lib/__tests__/web-server.test.ts +190 -0
  48. package/templates/agent/src/shared/lib/bash-security.ts +43 -43
  49. package/templates/agent/src/shared/lib/logger.ts +117 -95
  50. package/templates/agent/src/shared/lib/web-server.ts +128 -0
  51. package/templates/agent/src/shared/modules/debug.ts +8 -0
  52. package/templates/agent/src/shared/rpc-schema.ts +16 -3
  53. package/templates/agent/test-upload.txt +0 -0
  54. package/templates/agent/tsconfig.ipc.json +14 -0
  55. package/templates/agent/tsconfig.json +13 -3
  56. package/templates/chat/eslint.config.mjs +15 -2
  57. package/templates/chat/package.json +13 -2
  58. package/templates/chat/src/__tests__/hybrid-mode.test.ts +125 -0
  59. package/templates/chat/src/__tests__/server-config-security.test.ts +55 -0
  60. package/templates/chat/src/__tests__/server-config.test.ts +46 -44
  61. package/templates/chat/src/bun/index.ts +94 -44
  62. package/templates/chat/src/bun/three.d.ts +1 -0
  63. package/templates/chat/src/gateway/http-routes.ts +1 -1
  64. package/templates/chat/src/gateway/ws-handler.ts +84 -56
  65. package/templates/chat/src/mainview/App.tsx +6 -0
  66. package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  67. package/templates/chat/src/mainview/__tests__/theme-variables.test.ts +36 -0
  68. package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  69. package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +1 -0
  70. package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
  71. package/templates/chat/src/mainview/lib/api-client.ts +180 -156
  72. package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
  73. package/templates/chat/src/mainview/main.tsx +10 -6
  74. package/templates/chat/src/server-config.ts +33 -31
  75. package/templates/chat/src/server.ts +42 -60
  76. package/templates/chat/src/shared/handlers/chat.ts +196 -162
  77. package/templates/chat/src/shared/handlers/debug.ts +12 -0
  78. package/templates/chat/src/shared/handlers/index.ts +1 -0
  79. package/templates/chat/src/shared/http-routes.ts +250 -0
  80. package/templates/chat/src/shared/lib/__tests__/logger.test.ts +92 -0
  81. package/templates/chat/src/shared/lib/logger.ts +108 -95
  82. package/templates/chat/src/shared/lib/web-server.ts +128 -0
  83. package/templates/chat/src/shared/modules/debug.ts +8 -0
  84. package/templates/chat/src/shared/rpc-schema.ts +3 -2
  85. package/templates/chat/test-upload.txt +0 -0
  86. package/templates/chat/tsconfig.ipc.json +14 -0
  87. package/templates/chat/tsconfig.json +12 -2
  88. package/templates/general/eslint.config.mjs +15 -2
  89. package/templates/general/package.json +13 -2
  90. package/templates/general/src/__tests__/hybrid-mode.test.ts +125 -0
  91. package/templates/general/src/__tests__/server-config-security.test.ts +55 -0
  92. package/templates/general/src/__tests__/server-config.test.ts +46 -44
  93. package/templates/general/src/bun/index.ts +94 -44
  94. package/templates/general/src/bun/three.d.ts +1 -0
  95. package/templates/general/src/gateway/http-routes.ts +1 -1
  96. package/templates/general/src/gateway/ws-handler.ts +84 -56
  97. package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  98. package/templates/general/src/mainview/__tests__/theme-variables.test.ts +36 -0
  99. package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  100. package/templates/general/src/mainview/components/common/ThemeToggle.tsx +1 -0
  101. package/templates/general/src/mainview/components/feed/FeedPanel.tsx +241 -219
  102. package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
  103. package/templates/general/src/mainview/components/layout/AppLayout.tsx +6 -0
  104. package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
  105. package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
  106. package/templates/general/src/mainview/lib/api-client.ts +180 -156
  107. package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
  108. package/templates/general/src/mainview/main.tsx +10 -6
  109. package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
  110. package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
  111. package/templates/general/src/server-config.ts +33 -31
  112. package/templates/general/src/server.ts +42 -60
  113. package/templates/general/src/shared/handlers/__tests__/chat.test.ts +165 -160
  114. package/templates/general/src/shared/handlers/chat.ts +189 -155
  115. package/templates/general/src/shared/handlers/debug.ts +12 -0
  116. package/templates/general/src/shared/handlers/feed.ts +38 -40
  117. package/templates/general/src/shared/handlers/git.ts +286 -243
  118. package/templates/general/src/shared/handlers/index.ts +1 -0
  119. package/templates/general/src/shared/http-routes.ts +250 -0
  120. package/templates/general/src/shared/lib/__tests__/logger.test.ts +92 -0
  121. package/templates/general/src/shared/lib/logger.ts +117 -95
  122. package/templates/general/src/shared/lib/web-server.ts +128 -0
  123. package/templates/general/src/shared/modules/debug.ts +8 -0
  124. package/templates/general/src/shared/rpc-schema.ts +12 -2
  125. package/templates/general/test-upload.txt +0 -0
  126. package/templates/general/tsconfig.ipc.json +14 -0
  127. package/templates/general/tsconfig.json +12 -2
  128. package/templates/shared/__tests__/cors-security.test.ts +165 -0
  129. package/templates/shared/__tests__/file-path-security.test.ts +260 -0
  130. package/templates/shared/__tests__/http-routes.test.ts +174 -0
  131. package/templates/shared/__tests__/logger.test.ts +186 -0
  132. package/templates/shared/components/ErrorBoundary.tsx +59 -0
  133. package/templates/shared/env.d.ts +11 -0
  134. package/templates/shared/http-routes.ts +250 -0
  135. package/templates/shared/logger.ts +129 -0
  136. package/templates/shared/test-upload.txt +0 -0
  137. package/templates/shared/vite-base.config.ts +92 -0
  138. package/templates/shared/vitest-base.config.ts +42 -0
  139. package/templates/agent/eslint-plugin-rpc/index.js +0 -50
  140. package/templates/agent/eslint-plugin-rpc/package.json +0 -6
  141. package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  142. package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  143. package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  144. package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  145. package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  146. package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  147. package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  148. package/templates/chat/eslint-plugin-rpc/index.js +0 -50
  149. package/templates/chat/eslint-plugin-rpc/package.json +0 -6
  150. package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  151. package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  152. package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  153. package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  154. package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  155. package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  156. package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  157. package/templates/general/eslint-plugin-rpc/index.js +0 -50
  158. package/templates/general/eslint-plugin-rpc/package.json +0 -6
  159. package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  160. package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  161. package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  162. package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  163. package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  164. package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  165. package/templates/general/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
@@ -2,140 +2,167 @@ import { useState, useEffect } from "react";
2
2
  import { useTranslation } from "react-i18next";
3
3
  import { Plus, Trash2, Circle, Clock, CheckCircle2, ListTodo } from "lucide-react";
4
4
  import { useTodoStore } from "../../stores/use-todo-store";
5
- import type { TodoStatus } from "../../../shared/modules/todo";
5
+ import type { TodoStatus } from "@shared/modules/todo";
6
6
 
7
7
  const nextStatus: Record<TodoStatus, TodoStatus> = {
8
- pending: "in_progress",
9
- in_progress: "completed",
10
- completed: "pending",
8
+ pending: "in_progress",
9
+ in_progress: "completed",
10
+ completed: "pending",
11
11
  };
12
12
 
13
13
  const statusFlow = ["pending", "in_progress", "completed"] as const;
14
14
 
15
15
  export function TodoPanel() {
16
- const { t } = useTranslation();
17
- const [showInput, setShowInput] = useState(false);
18
- const [input, setInput] = useState("");
19
- const items = useTodoStore((s) => s.items);
20
- const fetchItems = useTodoStore((s) => s.fetchItems);
21
- const addItem = useTodoStore((s) => s.addItem);
22
- const updateItem = useTodoStore((s) => s.updateItem);
23
- const removeItem = useTodoStore((s) => s.removeItem);
16
+ const { t } = useTranslation();
17
+ const [showInput, setShowInput] = useState(false);
18
+ const [input, setInput] = useState("");
19
+ const items = useTodoStore((s) => s.items);
20
+ const fetchItems = useTodoStore((s) => s.fetchItems);
21
+ const addItem = useTodoStore((s) => s.addItem);
22
+ const updateItem = useTodoStore((s) => s.updateItem);
23
+ const removeItem = useTodoStore((s) => s.removeItem);
24
24
 
25
- useEffect(() => {
26
- fetchItems();
27
- }, [fetchItems]);
25
+ useEffect(() => {
26
+ fetchItems();
27
+ }, [fetchItems]);
28
28
 
29
- const handleAdd = () => {
30
- if (!input.trim()) return;
31
- addItem(input.trim());
32
- setInput("");
33
- setShowInput(false);
34
- };
29
+ const handleAdd = () => {
30
+ if (!input.trim()) return;
31
+ addItem(input.trim());
32
+ setInput("");
33
+ setShowInput(false);
34
+ };
35
35
 
36
- const handleKeyDown = (e: React.KeyboardEvent) => {
37
- if (e.key === "Enter") handleAdd();
38
- if (e.key === "Escape") setShowInput(false);
39
- };
36
+ const handleKeyDown = (e: React.KeyboardEvent) => {
37
+ if (e.key === "Enter") handleAdd();
38
+ if (e.key === "Escape") setShowInput(false);
39
+ };
40
40
 
41
- const statusConfig: Record<TodoStatus, { icon: typeof Circle; color: string; label: string; circleColor: string }> = {
42
- pending: { icon: Circle, color: "text-gray-400", label: t("todo.statusPending"), circleColor: "border-gray-500" },
43
- in_progress: { icon: Clock, color: "text-yellow-400", label: t("todo.statusInProgress"), circleColor: "border-yellow-400 bg-yellow-400/20" },
44
- completed: { icon: CheckCircle2, color: "text-green-400", label: t("todo.statusCompleted"), circleColor: "border-green-400 bg-green-400/20" },
45
- };
41
+ const statusConfig: Record<
42
+ TodoStatus,
43
+ { icon: typeof Circle; color: string; label: string; circleColor: string }
44
+ > = {
45
+ pending: {
46
+ icon: Circle,
47
+ color: "text-gray-400",
48
+ label: t("todo.statusPending"),
49
+ circleColor: "border-gray-500",
50
+ },
51
+ in_progress: {
52
+ icon: Clock,
53
+ color: "text-yellow-400",
54
+ label: t("todo.statusInProgress"),
55
+ circleColor: "border-yellow-400 bg-yellow-400/20",
56
+ },
57
+ completed: {
58
+ icon: CheckCircle2,
59
+ color: "text-green-400",
60
+ label: t("todo.statusCompleted"),
61
+ circleColor: "border-green-400 bg-green-400/20",
62
+ },
63
+ };
46
64
 
47
- return (
48
- <div className="flex flex-col h-full">
49
- <div className="p-3 border-b border-[var(--color-border-primary)] flex items-center justify-between">
50
- <span className="text-sm text-[var(--color-text-secondary)]">{t("todo.title")}</span>
51
- <button
52
- onClick={() => setShowInput(!showInput)}
53
- className="flex items-center gap-1 px-2 py-1 text-xs bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] rounded text-[var(--color-text-primary)]"
54
- >
55
- <Plus className="w-3 h-3" />
56
- {t("todo.add")}
57
- </button>
58
- </div>
65
+ return (
66
+ <div className="flex flex-col h-full">
67
+ <div className="p-3 border-b border-[var(--color-border-primary)] flex items-center justify-between">
68
+ <span className="text-sm text-[var(--color-text-secondary)]">{t("todo.title")}</span>
69
+ <button
70
+ onClick={() => setShowInput(!showInput)}
71
+ className="flex items-center gap-1 px-2 py-1 text-xs bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] rounded text-[var(--color-text-primary)]"
72
+ >
73
+ <Plus className="w-3 h-3" />
74
+ {t("todo.add")}
75
+ </button>
76
+ </div>
59
77
 
60
- {showInput && (
61
- <div className="p-3 border-b border-[var(--color-border-primary)] bg-[var(--color-bg-secondary)] flex items-center gap-2">
62
- <input
63
- type="text"
64
- value={input}
65
- onChange={(e) => setInput(e.target.value)}
66
- onKeyDown={handleKeyDown}
67
- placeholder={t("todo.placeholder")}
68
- autoFocus
69
- className="flex-1 bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] px-3 py-1.5 rounded text-sm border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none"
70
- />
71
- <button
72
- onClick={handleAdd}
73
- disabled={!input.trim()}
74
- className="px-3 py-1 text-xs bg-green-600 hover:bg-green-500 disabled:opacity-40 rounded text-white"
75
- >
76
- {t("todo.save")}
77
- </button>
78
- </div>
79
- )}
78
+ {showInput && (
79
+ <div className="p-3 border-b border-[var(--color-border-primary)] bg-[var(--color-bg-secondary)] flex items-center gap-2">
80
+ <input
81
+ type="text"
82
+ value={input}
83
+ onChange={(e) => setInput(e.target.value)}
84
+ onKeyDown={handleKeyDown}
85
+ placeholder={t("todo.placeholder")}
86
+ autoFocus
87
+ className="flex-1 bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] px-3 py-1.5 rounded text-sm border border-[var(--color-border-secondary)] focus:border-[var(--color-accent)] focus:outline-none"
88
+ />
89
+ <button
90
+ onClick={handleAdd}
91
+ disabled={!input.trim()}
92
+ className="px-3 py-1 text-xs bg-green-600 hover:bg-green-500 disabled:opacity-40 rounded text-white"
93
+ >
94
+ {t("todo.save")}
95
+ </button>
96
+ </div>
97
+ )}
80
98
 
81
- <div className="flex-1 overflow-auto p-3">
82
- {items.length === 0 ? (
83
- <div className="flex flex-col items-center justify-center h-full text-gray-600">
84
- <ListTodo className="w-12 h-12 mb-3 opacity-30" />
85
- <span className="text-sm">{t("todo.empty")}</span>
86
- <span className="text-xs mt-1">{t("todo.emptyHint")}</span>
87
- </div>
88
- ) : (
89
- <div className="space-y-2.5">
90
- {items.map((item) => {
91
- const cfg = statusConfig[item.status as TodoStatus];
92
- const Icon = cfg.icon;
93
- return (
94
- <div
95
- key={item.id}
96
- className="group relative bg-[var(--color-bg-secondary)]/60 border border-[var(--color-border-primary)]/80 rounded-lg p-3 hover:bg-[var(--color-bg-secondary)] hover:border-[var(--color-border-secondary)] transition-all"
97
- >
98
- <div className="flex items-start gap-3">
99
- <button
100
- onClick={() => updateItem(item.id, nextStatus[item.status as TodoStatus])}
101
- className={`flex-shrink-0 mt-0.5 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-all ${cfg.circleColor} ${cfg.color} hover:scale-110`}
102
- >
103
- <Icon className="w-3 h-3" />
104
- </button>
105
- <div className="flex-1 min-w-0">
106
- <p className={`text-sm leading-relaxed ${
107
- item.status === "completed" ? "text-[var(--color-text-placeholder)] line-through" : "text-[var(--color-text-secondary)]"
108
- }`}>
109
- {item.content}
110
- </p>
111
- <div className="flex items-center gap-1.5 mt-2 text-[11px] text-[var(--color-text-placeholder)]">
112
- <span>{t("todo.status")}</span>
113
- {statusFlow.map((s) => {
114
- const sc = statusConfig[s];
115
- const isActive = s === item.status;
116
- return (
117
- <span key={s} className={`inline-flex items-center gap-0.5 ${isActive ? sc.color + " font-medium" : ""}`}>
118
- {isActive && <sc.icon className="w-2.5 h-2.5" />}
119
- {sc.label}
120
- {s !== "completed" && <span className="text-gray-700 mx-0.5">&rarr;</span>}
121
- </span>
122
- );
123
- })}
124
- </div>
125
- </div>
126
- <button
127
- onClick={() => removeItem(item.id)}
128
- className="text-gray-600 hover:text-[var(--color-text-error)] opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 self-start mt-0.5"
129
- >
130
- <Trash2 className="w-3.5 h-3.5" />
131
- </button>
132
- </div>
133
- </div>
134
- );
135
- })}
136
- </div>
137
- )}
138
- </div>
139
- </div>
140
- );
99
+ <div className="flex-1 overflow-auto p-3">
100
+ {items.length === 0 ? (
101
+ <div className="flex flex-col items-center justify-center h-full text-gray-600">
102
+ <ListTodo className="w-12 h-12 mb-3 opacity-30" />
103
+ <span className="text-sm">{t("todo.empty")}</span>
104
+ <span className="text-xs mt-1">{t("todo.emptyHint")}</span>
105
+ </div>
106
+ ) : (
107
+ <div className="space-y-2.5">
108
+ {items.map((item) => {
109
+ const cfg = statusConfig[item.status as TodoStatus]!;
110
+ const Icon = cfg.icon;
111
+ return (
112
+ <div
113
+ key={item.id}
114
+ className="group relative bg-[var(--color-bg-secondary)]/60 border border-[var(--color-border-primary)]/80 rounded-lg p-3 hover:bg-[var(--color-bg-secondary)] hover:border-[var(--color-border-secondary)] transition-all"
115
+ >
116
+ <div className="flex items-start gap-3">
117
+ <button
118
+ onClick={() => updateItem(item.id, nextStatus[item.status as TodoStatus]!)}
119
+ className={`flex-shrink-0 mt-0.5 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-all ${cfg.circleColor} ${cfg.color} hover:scale-110`}
120
+ >
121
+ <Icon className="w-3 h-3" />
122
+ </button>
123
+ <div className="flex-1 min-w-0">
124
+ <p
125
+ className={`text-sm leading-relaxed ${
126
+ item.status === "completed"
127
+ ? "text-[var(--color-text-placeholder)] line-through"
128
+ : "text-[var(--color-text-secondary)]"
129
+ }`}
130
+ >
131
+ {item.content}
132
+ </p>
133
+ <div className="flex items-center gap-1.5 mt-2 text-[11px] text-[var(--color-text-placeholder)]">
134
+ <span>{t("todo.status")}</span>
135
+ {statusFlow.map((s) => {
136
+ const sc = statusConfig[s]!;
137
+ const isActive = s === item.status;
138
+ return (
139
+ <span
140
+ key={s}
141
+ className={`inline-flex items-center gap-0.5 ${isActive ? sc.color + " font-medium" : ""}`}
142
+ >
143
+ {isActive && <sc.icon className="w-2.5 h-2.5" />}
144
+ {sc.label}
145
+ {s !== "completed" && (
146
+ <span className="text-gray-700 mx-0.5">&rarr;</span>
147
+ )}
148
+ </span>
149
+ );
150
+ })}
151
+ </div>
152
+ </div>
153
+ <button
154
+ onClick={() => removeItem(item.id)}
155
+ className="text-gray-600 hover:text-[var(--color-text-error)] opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0 self-start mt-0.5"
156
+ >
157
+ <Trash2 className="w-3.5 h-3.5" />
158
+ </button>
159
+ </div>
160
+ </div>
161
+ );
162
+ })}
163
+ </div>
164
+ )}
165
+ </div>
166
+ </div>
167
+ );
141
168
  }
@@ -4,81 +4,90 @@ const HISTORY_KEY = "pi-input-history";
4
4
  const MAX_ITEMS = 10;
5
5
 
6
6
  function getStorageKey(sessionId: string): string {
7
- return `${HISTORY_KEY}:${sessionId}`;
7
+ return `${HISTORY_KEY}:${sessionId}`;
8
8
  }
9
9
 
10
10
  function readHistory(sessionId: string): string[] {
11
- try {
12
- const raw = localStorage.getItem(getStorageKey(sessionId));
13
- if (!raw) return [];
14
- const parsed = JSON.parse(raw);
15
- if (Array.isArray(parsed)) return parsed.slice(0, MAX_ITEMS);
16
- } catch { /* ignore */ }
17
- return [];
11
+ try {
12
+ const raw = localStorage.getItem(getStorageKey(sessionId));
13
+ if (!raw) return [];
14
+ const parsed = JSON.parse(raw);
15
+ if (Array.isArray(parsed)) return parsed.slice(0, MAX_ITEMS);
16
+ } catch {
17
+ /* ignore */
18
+ }
19
+ return [];
18
20
  }
19
21
 
20
22
  function writeHistory(sessionId: string, items: string[]) {
21
- try {
22
- localStorage.setItem(getStorageKey(sessionId), JSON.stringify(items.slice(0, MAX_ITEMS)));
23
- } catch { /* ignore */ }
23
+ try {
24
+ localStorage.setItem(getStorageKey(sessionId), JSON.stringify(items.slice(0, MAX_ITEMS)));
25
+ } catch {
26
+ /* ignore */
27
+ }
24
28
  }
25
29
 
26
30
  export function useInputHistory(sessionId: string) {
27
- const historyRef = useRef<string[]>(readHistory(sessionId));
28
- const indexRef = useRef(-1);
29
- const [, forceUpdate] = useState(0);
31
+ const historyRef = useRef<string[]>(readHistory(sessionId));
32
+ const indexRef = useRef(-1);
33
+ const [, forceUpdate] = useState(0);
30
34
 
31
- const hasPrev = historyRef.current.length > 0 && indexRef.current < historyRef.current.length - 1;
32
- const hasNext = indexRef.current > 0;
35
+ const hasPrev = historyRef.current.length > 0 && indexRef.current < historyRef.current.length - 1;
36
+ const hasNext = indexRef.current > 0;
33
37
 
34
- const saveToHistory = useCallback((text: string) => {
35
- const trimmed = text.trim();
36
- if (!trimmed) return;
37
- const h = historyRef.current;
38
- const filtered = h.filter((item) => item !== trimmed);
39
- const updated = [trimmed, ...filtered].slice(0, MAX_ITEMS);
40
- historyRef.current = updated;
41
- writeHistory(sessionId, updated);
42
- indexRef.current = -1;
43
- forceUpdate((n) => n + 1);
44
- }, [sessionId]);
38
+ const saveToHistory = useCallback(
39
+ (text: string) => {
40
+ const trimmed = text.trim();
41
+ if (!trimmed) return;
42
+ const h = historyRef.current;
43
+ const filtered = h.filter((item) => item !== trimmed);
44
+ const updated = [trimmed, ...filtered].slice(0, MAX_ITEMS);
45
+ historyRef.current = updated;
46
+ writeHistory(sessionId, updated);
47
+ indexRef.current = -1;
48
+ forceUpdate((n) => n + 1);
49
+ },
50
+ [sessionId]
51
+ );
45
52
 
46
- const navigatePrev = useCallback((): string | null => {
47
- const h = historyRef.current;
48
- if (h.length === 0) return null;
49
- const nextIdx = Math.min(indexRef.current + 1, h.length - 1);
50
- indexRef.current = nextIdx;
51
- forceUpdate((n) => n + 1);
52
- return h[nextIdx];
53
- }, []);
53
+ const navigatePrev = useCallback((): string | null => {
54
+ const h = historyRef.current;
55
+ if (h.length === 0) return null;
56
+ const nextIdx = Math.min(indexRef.current + 1, h.length - 1);
57
+ indexRef.current = nextIdx;
58
+ forceUpdate((n) => n + 1);
59
+ return h[nextIdx] ?? null;
60
+ }, []);
54
61
 
55
- const navigateNext = useCallback((): string | null => {
56
- const h = historyRef.current;
57
- if (h.length === 0) return null;
58
- const nextIdx = indexRef.current - 1;
59
- if (nextIdx < 0) {
60
- indexRef.current = -1;
61
- forceUpdate((n) => n + 1);
62
- return "";
63
- }
64
- indexRef.current = nextIdx;
65
- forceUpdate((n) => n + 1);
66
- return h[nextIdx];
67
- }, []);
62
+ const navigateNext = useCallback((): string | null => {
63
+ const h = historyRef.current;
64
+ if (h.length === 0) return null;
65
+ const nextIdx = indexRef.current - 1;
66
+ if (nextIdx < 0) {
67
+ indexRef.current = -1;
68
+ forceUpdate((n) => n + 1);
69
+ return "";
70
+ }
71
+ indexRef.current = nextIdx;
72
+ forceUpdate((n) => n + 1);
73
+ return h[nextIdx] ?? null;
74
+ }, []);
68
75
 
69
- const clearHistory = useCallback(() => {
70
- historyRef.current = [];
71
- indexRef.current = -1;
72
- try {
73
- localStorage.removeItem(getStorageKey(sessionId));
74
- } catch { /* ignore */ }
75
- forceUpdate((n) => n + 1);
76
- }, [sessionId]);
76
+ const clearHistory = useCallback(() => {
77
+ historyRef.current = [];
78
+ indexRef.current = -1;
79
+ try {
80
+ localStorage.removeItem(getStorageKey(sessionId));
81
+ } catch {
82
+ /* ignore */
83
+ }
84
+ forceUpdate((n) => n + 1);
85
+ }, [sessionId]);
77
86
 
78
- const resetIndex = useCallback(() => {
79
- indexRef.current = -1;
80
- forceUpdate((n) => n + 1);
81
- }, []);
87
+ const resetIndex = useCallback(() => {
88
+ indexRef.current = -1;
89
+ forceUpdate((n) => n + 1);
90
+ }, []);
82
91
 
83
- return { saveToHistory, navigatePrev, navigateNext, clearHistory, resetIndex, hasPrev, hasNext };
92
+ return { saveToHistory, navigatePrev, navigateNext, clearHistory, resetIndex, hasPrev, hasNext };
84
93
  }