@yancyyu/openhermit 1.6.26 → 1.6.28
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/README.md +132 -80
- package/bin/hermit.mjs +2 -2
- package/dist-renderer/assets/{ProjectEditorOverlay-Byepdwo2.js → ProjectEditorOverlay-A4DZTvSy.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-vvWu-2c9.js → TeamGraphOverlay-Ba5njic5.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-DfsmMgXN.js → _basePickBy-BvnK-OC1.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-Bve-IKz5.js → _baseUniq-DmFYXx9G.js} +1 -1
- package/dist-renderer/assets/{arc-4cbkhagw.js → arc-DX4ZQFY4.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-CC9i0bMK.js → architectureDiagram-VXUJARFQ-DfYr3vEN.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-BjFruJ65.js → blockDiagram-VD42YOAC-DuXdVeWn.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CrYzsQC1.js → c4Diagram-YG6GDRKO-Bw2nixXe.js} +1 -1
- package/dist-renderer/assets/channel-Pre42N5O.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-Bb9MCt7J.js → chunk-4BX2VUAB-DLiNGQoE.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-BpOVOXVa.js → chunk-55IACEB6-B1L_8VIF.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-GtEiO-7n.js → chunk-B4BG7PRW-DaZMWKGk.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-BRlzcOEj.js → chunk-DI55MBZ5-ku-dflJG.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-DcvMVOZx.js → chunk-FMBD7UC4-DV-mF1dP.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-B9pkjVpd.js → chunk-QN33PNHL-ByGcDFQ0.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DzHPSm01.js → chunk-QZHKN3VN-7dv-Min8.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BU9c0Hcn.js → chunk-TZMSLE5B-WdXL5fTu.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +1 -0
- package/dist-renderer/assets/clone-BjQBiNfj.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BqOg0x3V.js → cose-bilkent-S5V4N54A-CNcsvqPl.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-C9JTWefj.js → dagre-6UL2VRFP-DBNx4qqx.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-ljleG6ui.js → diagram-PSM6KHXK-BfVlT6sT.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-BbV-WSTr.js → diagram-QEK2KX5R-HvVjs0K6.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-CKi3DFby.js → diagram-S2PKOQOG-DYb_KnWS.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-D3HE7b-j.js → erDiagram-Q2GNP2WA-Ba-IgI5G.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-C2yLRmM0.js → flowDiagram-NV44I4VS-2iDN8Kpj.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-XEV4KtUf.js → ganttDiagram-JELNMOA3-Byjf8Fa3.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-ufaCCg7c.js → gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js} +1 -1
- package/dist-renderer/assets/{graph-BzPvdBp0.js → graph-Enirf-f8.js} +1 -1
- package/dist-renderer/assets/{index-BprOls_t.js → index-AjxP_rE_.js} +1 -1
- package/dist-renderer/assets/index-BIOJremZ.css +1 -0
- package/dist-renderer/assets/{index-DHq6dXy7.js → index-COZPUWJW.js} +1 -1
- package/dist-renderer/assets/{index-Cr91T9ef.js → index-ChR1D6ZF.js} +1 -1
- package/dist-renderer/assets/{index-DUIDxnaf.js → index-CtlzGepK.js} +1 -1
- package/dist-renderer/assets/{index-A5CMVuXA.js → index-DY1zqsb6.js} +538 -538
- package/dist-renderer/assets/{index-yNYjzR2R.js → index-DdhqolqE.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DKP5zgHc.js → infoDiagram-HS3SLOUP-D6uicwz1.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-Omd7tmzE.js → journeyDiagram-XKPGCS4Q-DqwZsXlQ.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-D7yw9yIY.js → kanban-definition-3W4ZIXB7-fCDVhVUm.js} +1 -1
- package/dist-renderer/assets/{layout-DZxAqFuM.js → layout-CPFgj98r.js} +1 -1
- package/dist-renderer/assets/{linear-BXWJygRB.js → linear-CYiQ7Y3M.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-BfJ09SBb.js → mindmap-definition-VGOIOE7T-D31dS2KE.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BYaLQhXj.js → pieDiagram-ADFJNKIX-BOsCJfds.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DeA0B1fw.js → quadrantDiagram-AYHSOK5B-CYTVQCfr.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-DnFWn7-v.js → requirementDiagram-UZGBJVZJ-CODCFpkt.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-L9bek20k.js → sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BBmcJUXb.js → sequenceDiagram-WL72ISMW-CmS9TxhW.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-DrwPQvTq.js → stateDiagram-FKZM4ZOC-o9k-ns3q.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-BOUQrTH6.js → stateDiagram-v2-4FDKWEC3-CxHMyEt1.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-Dldh9vsj.js → timeline-definition-IT6M3QCI-B6T3zrde.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-BsGSs8-P.js → treemap-GDKQZRPO-CVd5GNDw.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-BsR_bj-d.js → xychartDiagram-PRI3JC2R-CleBrdqc.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/server.ts +137 -8
- package/src/main/services/session-intelligence/SessionUsageParser.ts +446 -0
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +237 -0
- package/src/renderer/api/httpClient.ts +1 -1
- package/src/renderer/components/dashboard/DashboardView.tsx +6 -105
- package/src/renderer/components/settings/SettingsTabs.tsx +2 -2
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +463 -83
- package/src/renderer/components/team/TeamDetailView.tsx +40 -20
- package/src/renderer/components/team/TeamListView.tsx +35 -21
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +10 -7
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +9 -6
- package/src/shared/types/team.ts +5 -0
- package/dist-renderer/assets/channel-BMMyVRy4.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-Dz1VG1T3.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-Dz1VG1T3.js +0 -1
- package/dist-renderer/assets/clone-COsIIGZQ.js +0 -1
- package/dist-renderer/assets/index-CWpFqEvz.css +0 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UsageTelemetryService - scans Claude Code JSONL sessions and uploads
|
|
3
|
+
* metadata-only usage metrics to Redis.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type Redis from 'ioredis';
|
|
7
|
+
import { scanSessions } from './SessionUsageParser';
|
|
8
|
+
import type { TaskBusConfig } from '@shared/types/team';
|
|
9
|
+
import type { ParseResult } from './SessionUsageParser';
|
|
10
|
+
|
|
11
|
+
const KEY_DAILY = (slug: string, date: string) => `hermit:usage:${slug}:daily:${date}`;
|
|
12
|
+
const KEY_SUMMARY = (slug: string) => `hermit:usage:${slug}:summary`;
|
|
13
|
+
const KEY_LAST_SCAN = (slug: string) => `hermit:usage:${slug}:lastScan`;
|
|
14
|
+
const KEY_HOURLY = (slug: string) => `hermit:usage:${slug}:hourly`;
|
|
15
|
+
const KEY_EVENTS7D = (slug: string) => `hermit:usage:${slug}:events7d`;
|
|
16
|
+
const KEY_WORK_SECONDS = (slug: string) => `hermit:usage:${slug}:workSeconds`;
|
|
17
|
+
const KEY_PROJECTS = (slug: string) => `hermit:usage:${slug}:projects`;
|
|
18
|
+
|
|
19
|
+
let scanInterval: ReturnType<typeof setInterval> | null = null;
|
|
20
|
+
|
|
21
|
+
function redisConfig(cfg: TaskBusConfig) {
|
|
22
|
+
return {
|
|
23
|
+
host: cfg.redis.host,
|
|
24
|
+
port: cfg.redis.port,
|
|
25
|
+
password: cfg.redis.password,
|
|
26
|
+
db: cfg.redis.db,
|
|
27
|
+
lazyConnect: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function getRedis(cfg: TaskBusConfig): Promise<Redis | null> {
|
|
32
|
+
let Redis: typeof import('ioredis').default;
|
|
33
|
+
try {
|
|
34
|
+
const mod = await import('ioredis');
|
|
35
|
+
Redis = mod.default;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const r = new Redis(redisConfig(cfg));
|
|
41
|
+
try {
|
|
42
|
+
await r.connect();
|
|
43
|
+
await r.ping();
|
|
44
|
+
return r;
|
|
45
|
+
} catch {
|
|
46
|
+
try {
|
|
47
|
+
r.disconnect();
|
|
48
|
+
} catch {
|
|
49
|
+
/* ignore */
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function uploadMetrics(client: Redis, slug: string, result: ParseResult): Promise<void> {
|
|
56
|
+
const { aggregate } = result;
|
|
57
|
+
const pipe = client.pipeline();
|
|
58
|
+
|
|
59
|
+
// Per-day metrics (90 day TTL)
|
|
60
|
+
for (const [day, m] of Object.entries(aggregate.daily)) {
|
|
61
|
+
pipe.hset(KEY_DAILY(slug, day), {
|
|
62
|
+
sessions: m.sessions,
|
|
63
|
+
messages: m.messages,
|
|
64
|
+
tokens_in: m.tokensIn,
|
|
65
|
+
tokens_out: m.tokensOut,
|
|
66
|
+
cache_read: m.cacheRead,
|
|
67
|
+
cache_creation: m.cacheCreation,
|
|
68
|
+
work_seconds: m.workSeconds,
|
|
69
|
+
});
|
|
70
|
+
pipe.expire(KEY_DAILY(slug, day), 90 * 86400);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Summary
|
|
74
|
+
pipe.hset(KEY_SUMMARY(slug), {
|
|
75
|
+
sessions: aggregate.sessions,
|
|
76
|
+
messages: aggregate.messages,
|
|
77
|
+
tokens_in: aggregate.tokens.input,
|
|
78
|
+
tokens_out: aggregate.tokens.output,
|
|
79
|
+
cache_read: aggregate.tokens.cacheRead,
|
|
80
|
+
cache_creation: aggregate.tokens.cacheCreation,
|
|
81
|
+
active_days: aggregate.activeDays,
|
|
82
|
+
last_scan: new Date().toISOString(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Hourly distribution
|
|
86
|
+
pipe.set(KEY_HOURLY(slug), JSON.stringify(aggregate.hourly));
|
|
87
|
+
|
|
88
|
+
// 7-day events for rolling window
|
|
89
|
+
pipe.set(KEY_EVENTS7D(slug), JSON.stringify(aggregate.events7d));
|
|
90
|
+
|
|
91
|
+
// Work seconds by day
|
|
92
|
+
pipe.set(KEY_WORK_SECONDS(slug), JSON.stringify(aggregate.workSecondsByDay));
|
|
93
|
+
|
|
94
|
+
// Projects ranking
|
|
95
|
+
pipe.set(KEY_PROJECTS(slug), JSON.stringify(aggregate.projects));
|
|
96
|
+
|
|
97
|
+
// Last scan time
|
|
98
|
+
pipe.set(KEY_LAST_SCAN(slug), new Date().toISOString());
|
|
99
|
+
|
|
100
|
+
await pipe.exec();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function doScan(cfg: TaskBusConfig): Promise<ParseResult | null> {
|
|
104
|
+
if (!cfg.telemetry?.enabled) return null;
|
|
105
|
+
|
|
106
|
+
const client = await getRedis(cfg);
|
|
107
|
+
if (!client) return null;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await scanSessions();
|
|
111
|
+
await uploadMetrics(client, 'global', result);
|
|
112
|
+
return result;
|
|
113
|
+
} finally {
|
|
114
|
+
try {
|
|
115
|
+
client.disconnect();
|
|
116
|
+
} catch {
|
|
117
|
+
/* ignore */
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function startTelemetry(cfg: TaskBusConfig): Promise<void> {
|
|
123
|
+
await stopTelemetry();
|
|
124
|
+
if (!cfg.telemetry?.enabled) return;
|
|
125
|
+
|
|
126
|
+
// Immediate first scan
|
|
127
|
+
await doScan(cfg);
|
|
128
|
+
|
|
129
|
+
// Periodic scan every 10 minutes
|
|
130
|
+
scanInterval = setInterval(
|
|
131
|
+
async () => {
|
|
132
|
+
await doScan(cfg);
|
|
133
|
+
},
|
|
134
|
+
10 * 60 * 1000
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function stopTelemetry(): Promise<void> {
|
|
139
|
+
if (scanInterval) {
|
|
140
|
+
clearInterval(scanInterval);
|
|
141
|
+
scanInterval = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function triggerScan(cfg: TaskBusConfig): Promise<ParseResult | null> {
|
|
146
|
+
return doScan(cfg);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function isTelemetryRunning(): boolean {
|
|
150
|
+
return scanInterval !== null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface TelemetryStatusResult {
|
|
154
|
+
connected: boolean;
|
|
155
|
+
lastScan: string | null;
|
|
156
|
+
sessions: number;
|
|
157
|
+
messages: number;
|
|
158
|
+
tokensIn: number;
|
|
159
|
+
tokensOut: number;
|
|
160
|
+
cacheRead: number;
|
|
161
|
+
cacheCreation: number;
|
|
162
|
+
activeDays: number;
|
|
163
|
+
hourly: number[];
|
|
164
|
+
projects: Array<{
|
|
165
|
+
cwd: string;
|
|
166
|
+
sessions: number;
|
|
167
|
+
messages: number;
|
|
168
|
+
tokensIn: number;
|
|
169
|
+
tokensOut: number;
|
|
170
|
+
}>;
|
|
171
|
+
workSecondsByDay: Record<string, number>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function getTelemetryStatus(
|
|
175
|
+
redisCfg: TaskBusConfig['redis']
|
|
176
|
+
): Promise<TelemetryStatusResult | null> {
|
|
177
|
+
let Redis: typeof import('ioredis').default;
|
|
178
|
+
try {
|
|
179
|
+
const mod = await import('ioredis');
|
|
180
|
+
Redis = mod.default;
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const cfg = { redis: redisCfg };
|
|
186
|
+
const client = new Redis(redisConfig(cfg as TaskBusConfig));
|
|
187
|
+
try {
|
|
188
|
+
await client.connect();
|
|
189
|
+
await client.ping();
|
|
190
|
+
} catch {
|
|
191
|
+
return {
|
|
192
|
+
connected: false,
|
|
193
|
+
lastScan: null,
|
|
194
|
+
sessions: 0,
|
|
195
|
+
messages: 0,
|
|
196
|
+
tokensIn: 0,
|
|
197
|
+
tokensOut: 0,
|
|
198
|
+
cacheRead: 0,
|
|
199
|
+
cacheCreation: 0,
|
|
200
|
+
activeDays: 0,
|
|
201
|
+
hourly: [],
|
|
202
|
+
projects: [],
|
|
203
|
+
workSecondsByDay: {},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const [lastScan, summary, hourlyRaw, projectsRaw, workSecondsRaw] = await Promise.all([
|
|
209
|
+
client.get(KEY_LAST_SCAN('global')),
|
|
210
|
+
client.hgetall(KEY_SUMMARY('global')),
|
|
211
|
+
client.get(KEY_HOURLY('global')),
|
|
212
|
+
client.get(KEY_PROJECTS('global')),
|
|
213
|
+
client.get(KEY_WORK_SECONDS('global')),
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
connected: true,
|
|
218
|
+
lastScan: lastScan ?? null,
|
|
219
|
+
sessions: Number(summary.sessions ?? 0),
|
|
220
|
+
messages: Number(summary.messages ?? 0),
|
|
221
|
+
tokensIn: Number(summary.tokens_in ?? 0),
|
|
222
|
+
tokensOut: Number(summary.tokens_out ?? 0),
|
|
223
|
+
cacheRead: Number(summary.cache_read ?? 0),
|
|
224
|
+
cacheCreation: Number(summary.cache_creation ?? 0),
|
|
225
|
+
activeDays: Number(summary.active_days ?? 0),
|
|
226
|
+
hourly: hourlyRaw ? JSON.parse(hourlyRaw) : new Array(24).fill(0),
|
|
227
|
+
projects: projectsRaw ? JSON.parse(projectsRaw) : [],
|
|
228
|
+
workSecondsByDay: workSecondsRaw ? JSON.parse(workSecondsRaw) : {},
|
|
229
|
+
};
|
|
230
|
+
} finally {
|
|
231
|
+
try {
|
|
232
|
+
client.disconnect();
|
|
233
|
+
} catch {
|
|
234
|
+
/* ignore */
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -278,7 +278,7 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
278
278
|
try {
|
|
279
279
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
280
280
|
method: 'DELETE',
|
|
281
|
-
headers: { 'Content-Type': 'application/json' },
|
|
281
|
+
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
282
282
|
body: body ? JSON.stringify(body) : undefined,
|
|
283
283
|
signal: controller.signal,
|
|
284
284
|
});
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* Keeps only screen composition and delegates recent-projects logic to the feature slice.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React
|
|
6
|
+
import React from 'react';
|
|
7
7
|
|
|
8
8
|
import { RecentProjectsSection } from '@features/recent-projects/renderer';
|
|
9
9
|
import { useStore } from '@renderer/store';
|
|
10
|
-
import {
|
|
11
|
-
import { Command, PlugZap, Search, Sparkles, Users, Workflow } from 'lucide-react';
|
|
10
|
+
import { PlugZap, Sparkles, Users, Workflow } from 'lucide-react';
|
|
12
11
|
import { useShallow } from 'zustand/react/shallow';
|
|
13
12
|
|
|
14
13
|
const HIGHLIGHT_HARNESSES = [
|
|
@@ -30,92 +29,7 @@ const HIGHLIGHT_CHANNELS = [
|
|
|
30
29
|
'Webhook / API',
|
|
31
30
|
];
|
|
32
31
|
|
|
33
|
-
interface CommandSearchProps {
|
|
34
|
-
value: string;
|
|
35
|
-
onChange: (value: string) => void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const CommandSearch = ({ value, onChange }: Readonly<CommandSearchProps>): React.JSX.Element => {
|
|
39
|
-
const [isFocused, setIsFocused] = useState(false);
|
|
40
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
41
|
-
const { openCommandPalette, selectedProjectId } = useStore(
|
|
42
|
-
useShallow((state) => ({
|
|
43
|
-
openCommandPalette: state.openCommandPalette,
|
|
44
|
-
selectedProjectId: state.selectedProjectId,
|
|
45
|
-
}))
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
const handleKeyDown = (event: KeyboardEvent): void => {
|
|
50
|
-
if ((event.metaKey || event.ctrlKey) && event.code === 'KeyK') {
|
|
51
|
-
event.preventDefault();
|
|
52
|
-
openCommandPalette();
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
57
|
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
58
|
-
}, [openCommandPalette]);
|
|
59
|
-
|
|
60
|
-
useLayoutEffect(() => {
|
|
61
|
-
const input = inputRef.current;
|
|
62
|
-
if (!input) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
input.focus({ preventScroll: true });
|
|
67
|
-
const timeoutId = window.setTimeout(() => {
|
|
68
|
-
if (document.activeElement !== input) {
|
|
69
|
-
input.focus({ preventScroll: true });
|
|
70
|
-
}
|
|
71
|
-
}, 50);
|
|
72
|
-
|
|
73
|
-
return () => window.clearTimeout(timeoutId);
|
|
74
|
-
}, []);
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<div className="relative w-full">
|
|
78
|
-
<div
|
|
79
|
-
className={`relative flex items-center gap-3 rounded-sm border bg-surface-raised px-4 py-3 transition-all duration-200 ${
|
|
80
|
-
isFocused
|
|
81
|
-
? 'border-zinc-500 shadow-[0_0_20px_rgba(255,255,255,0.04)] ring-1 ring-zinc-600/30'
|
|
82
|
-
: 'border-border hover:border-zinc-600'
|
|
83
|
-
} `}
|
|
84
|
-
>
|
|
85
|
-
<Search className="size-4 shrink-0 text-text-muted" />
|
|
86
|
-
<input
|
|
87
|
-
ref={inputRef}
|
|
88
|
-
type="text"
|
|
89
|
-
value={value}
|
|
90
|
-
onChange={(event) => onChange(event.target.value)}
|
|
91
|
-
placeholder="搜索项目..."
|
|
92
|
-
className="flex-1 bg-transparent text-sm text-text outline-none placeholder:text-text-muted"
|
|
93
|
-
onFocus={() => setIsFocused(true)}
|
|
94
|
-
onBlur={() => setIsFocused(false)}
|
|
95
|
-
/>
|
|
96
|
-
<button
|
|
97
|
-
onClick={() => openCommandPalette()}
|
|
98
|
-
className="flex shrink-0 items-center gap-1 transition-opacity hover:opacity-80"
|
|
99
|
-
title={
|
|
100
|
-
selectedProjectId
|
|
101
|
-
? `搜索会话(${formatShortcut('K')})`
|
|
102
|
-
: `搜索项目(${formatShortcut('K')})`
|
|
103
|
-
}
|
|
104
|
-
>
|
|
105
|
-
<kbd className="flex h-5 items-center justify-center rounded border border-border bg-surface-overlay px-1.5 text-[10px] font-medium text-text-muted">
|
|
106
|
-
<Command className="size-2.5" />
|
|
107
|
-
</kbd>
|
|
108
|
-
<kbd className="flex size-5 items-center justify-center rounded border border-border bg-surface-overlay text-[10px] font-medium text-text-muted">
|
|
109
|
-
K
|
|
110
|
-
</kbd>
|
|
111
|
-
</button>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
32
|
export const DashboardView = (): React.JSX.Element => {
|
|
118
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
119
33
|
const { openTeamsTab, openSettingsTab, teams, teamsLoading } = useStore(
|
|
120
34
|
useShallow((state) => ({
|
|
121
35
|
openTeamsTab: state.openTeamsTab,
|
|
@@ -124,8 +38,7 @@ export const DashboardView = (): React.JSX.Element => {
|
|
|
124
38
|
teamsLoading: state.teamsLoading,
|
|
125
39
|
}))
|
|
126
40
|
);
|
|
127
|
-
const showQuickstartGuide =
|
|
128
|
-
searchQuery.trim().length === 0 && !teamsLoading && teams.length === 0;
|
|
41
|
+
const showQuickstartGuide = !teamsLoading && teams.length === 0;
|
|
129
42
|
|
|
130
43
|
return (
|
|
131
44
|
<div className="relative flex-1 overflow-auto bg-surface">
|
|
@@ -201,7 +114,7 @@ export const DashboardView = (): React.JSX.Element => {
|
|
|
201
114
|
</div>
|
|
202
115
|
</section>
|
|
203
116
|
|
|
204
|
-
<div className="mb-
|
|
117
|
+
<div className="mb-8 flex items-center justify-center">
|
|
205
118
|
<button
|
|
206
119
|
onClick={openTeamsTab}
|
|
207
120
|
className="flex shrink-0 items-center gap-2 rounded-sm border border-border bg-surface-raised px-4 py-3 text-sm text-text-secondary transition-all duration-200 hover:border-zinc-500 hover:text-text"
|
|
@@ -209,10 +122,6 @@ export const DashboardView = (): React.JSX.Element => {
|
|
|
209
122
|
<Users className="size-4" />
|
|
210
123
|
选择团队
|
|
211
124
|
</button>
|
|
212
|
-
<span className="shrink-0 text-xs text-text-muted">或</span>
|
|
213
|
-
<div className="flex-1">
|
|
214
|
-
<CommandSearch value={searchQuery} onChange={setSearchQuery} />
|
|
215
|
-
</div>
|
|
216
125
|
</div>
|
|
217
126
|
|
|
218
127
|
{showQuickstartGuide ? (
|
|
@@ -255,19 +164,11 @@ export const DashboardView = (): React.JSX.Element => {
|
|
|
255
164
|
<>
|
|
256
165
|
<div className="mb-4 flex items-center justify-between">
|
|
257
166
|
<h2 className="text-xs font-medium uppercase tracking-wider text-text-muted">
|
|
258
|
-
|
|
167
|
+
最近项目
|
|
259
168
|
</h2>
|
|
260
|
-
{searchQuery.trim() && (
|
|
261
|
-
<button
|
|
262
|
-
onClick={() => setSearchQuery('')}
|
|
263
|
-
className="text-xs text-text-muted transition-colors hover:text-text-secondary"
|
|
264
|
-
>
|
|
265
|
-
清除搜索
|
|
266
|
-
</button>
|
|
267
|
-
)}
|
|
268
169
|
</div>
|
|
269
170
|
|
|
270
|
-
<RecentProjectsSection searchQuery=
|
|
171
|
+
<RecentProjectsSection searchQuery="" />
|
|
271
172
|
</>
|
|
272
173
|
)}
|
|
273
174
|
</div>
|