ikanban-web 0.2.8 → 0.2.10
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/dist/assets/{ghostty-web--axxBQj6.js → ghostty-web-CkHg_Jfr.js} +1 -1
- package/dist/assets/home-BTCNns6Z.js +1 -0
- package/dist/assets/index-DbuHW0gF.css +1 -0
- package/dist/assets/{index-BR3vRMWX.js → index-n2BMSLCz.js} +108 -108
- package/dist/assets/session-S93i-VnN.js +24 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/app.tsx +5 -1
- package/src/context/layout.tsx +12 -2
- package/src/i18n/en.ts +173 -83
- package/src/i18n/zh.ts +98 -47
- package/src/pages/home.tsx +287 -87
- package/src/pages/layout.tsx +1 -64
- package/src/pages/session/message-timeline.tsx +2 -1
- package/src/pages/session/review-tab.tsx +4 -0
- package/src/pages/session.tsx +13 -0
- package/dist/assets/home-BAEIhduF.js +0 -1
- package/dist/assets/index-B0TEjAod.css +0 -1
- package/dist/assets/session-Bp9iU6SU.js +0 -24
package/src/pages/home.tsx
CHANGED
|
@@ -1,56 +1,131 @@
|
|
|
1
|
-
import { createMemo, For, Match, Switch } from "solid-js"
|
|
2
|
-
import { Button } from "ikanban-ui/button"
|
|
3
|
-
import { Logo } from "ikanban-ui/logo"
|
|
4
|
-
import { useLayout } from "@/context/layout"
|
|
5
|
-
import { useNavigate } from "@solidjs/router"
|
|
6
|
-
import { base64Encode } from "ikanban-utils/encode"
|
|
7
|
-
import { Icon } from "ikanban-ui/icon"
|
|
8
|
-
import { usePlatform } from "@/context/platform"
|
|
9
|
-
import { DateTime } from "luxon"
|
|
10
|
-
import { useDialog } from "ikanban-ui/context/dialog"
|
|
11
|
-
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
|
12
|
-
import { DialogSelectServer } from "@/components/dialog-select-server"
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
1
|
+
import { createMemo, createEffect, For, Match, Switch } from "solid-js";
|
|
2
|
+
import { Button } from "ikanban-ui/button";
|
|
3
|
+
import { Logo } from "ikanban-ui/logo";
|
|
4
|
+
import { useLayout } from "@/context/layout";
|
|
5
|
+
import { useNavigate } from "@solidjs/router";
|
|
6
|
+
import { base64Encode } from "ikanban-utils/encode";
|
|
7
|
+
import { Icon } from "ikanban-ui/icon";
|
|
8
|
+
import { usePlatform } from "@/context/platform";
|
|
9
|
+
import { DateTime } from "luxon";
|
|
10
|
+
import { useDialog } from "ikanban-ui/context/dialog";
|
|
11
|
+
import { DialogSelectDirectory } from "@/components/dialog-select-directory";
|
|
12
|
+
import { DialogSelectServer } from "@/components/dialog-select-server";
|
|
13
|
+
import { useGlobalSDK } from "@/context/global-sdk";
|
|
14
|
+
import { useServer } from "@/context/server";
|
|
15
|
+
import { useGlobalSync } from "@/context/global-sync";
|
|
16
|
+
import { useLanguage } from "@/context/language";
|
|
17
|
+
import { IconButton } from "ikanban-ui/icon-button";
|
|
18
|
+
import { sortedRootSessions } from "@/pages/layout/helpers";
|
|
19
|
+
import type { Session } from "@opencode-ai/sdk/v2/client";
|
|
20
|
+
|
|
21
|
+
type BoardColumn = "progress" | "idle";
|
|
22
|
+
|
|
23
|
+
type BoardCard = {
|
|
24
|
+
session: Session;
|
|
25
|
+
projectDirectory: string;
|
|
26
|
+
updatedAt: number;
|
|
27
|
+
};
|
|
16
28
|
|
|
17
29
|
export default function Home() {
|
|
18
|
-
const sync = useGlobalSync()
|
|
19
|
-
const layout = useLayout()
|
|
20
|
-
const platform = usePlatform()
|
|
21
|
-
const dialog = useDialog()
|
|
22
|
-
const navigate = useNavigate()
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
30
|
+
const sync = useGlobalSync();
|
|
31
|
+
const layout = useLayout();
|
|
32
|
+
const platform = usePlatform();
|
|
33
|
+
const dialog = useDialog();
|
|
34
|
+
const navigate = useNavigate();
|
|
35
|
+
const globalSDK = useGlobalSDK();
|
|
36
|
+
const server = useServer();
|
|
37
|
+
const language = useLanguage();
|
|
26
38
|
const recent = createMemo(() => {
|
|
27
39
|
return sync.data.project
|
|
28
40
|
.slice()
|
|
29
|
-
.sort(
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
.sort(
|
|
42
|
+
(a, b) =>
|
|
43
|
+
(b.time.updated ?? b.time.created) -
|
|
44
|
+
(a.time.updated ?? a.time.created),
|
|
45
|
+
)
|
|
46
|
+
.slice(0, 5);
|
|
47
|
+
});
|
|
48
|
+
const boardColumns = createMemo<Record<BoardColumn, BoardCard[]>>(() => {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const columns: Record<BoardColumn, BoardCard[]> = {
|
|
51
|
+
progress: [],
|
|
52
|
+
idle: [],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const project of recent()) {
|
|
56
|
+
const [store] = sync.child(project.worktree);
|
|
57
|
+
const sessions = sortedRootSessions(store, now);
|
|
58
|
+
|
|
59
|
+
for (const session of sessions) {
|
|
60
|
+
const status = store.session_status[session.id];
|
|
61
|
+
const updatedAt = session.time.updated ?? session.time.created;
|
|
62
|
+
const card = {
|
|
63
|
+
session,
|
|
64
|
+
projectDirectory: project.worktree,
|
|
65
|
+
updatedAt,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (status?.type === "busy" || status?.type === "retry") {
|
|
69
|
+
columns.progress.push(card);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
columns.idle.push(card);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const key of Object.keys(columns) as BoardColumn[]) {
|
|
78
|
+
columns[key].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return columns;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
createEffect(() => {
|
|
85
|
+
for (const project of recent()) {
|
|
86
|
+
sync.child(project.worktree);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
32
89
|
|
|
33
90
|
const serverDotClass = createMemo(() => {
|
|
34
|
-
const healthy = server.healthy()
|
|
35
|
-
if (healthy === true) return "bg-icon-success-base"
|
|
36
|
-
if (healthy === false) return "bg-icon-critical-base"
|
|
37
|
-
return "bg-border-weak-base"
|
|
38
|
-
})
|
|
91
|
+
const healthy = server.healthy();
|
|
92
|
+
if (healthy === true) return "bg-icon-success-base";
|
|
93
|
+
if (healthy === false) return "bg-icon-critical-base";
|
|
94
|
+
return "bg-border-weak-base";
|
|
95
|
+
});
|
|
39
96
|
|
|
40
97
|
function openProject(directory: string) {
|
|
41
|
-
layout.projects.open(directory)
|
|
42
|
-
server.projects.touch(directory)
|
|
43
|
-
navigate(`/${base64Encode(directory)}`)
|
|
98
|
+
layout.projects.open(directory);
|
|
99
|
+
server.projects.touch(directory);
|
|
100
|
+
navigate(`/${base64Encode(directory)}/session`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function openSession(directory: string, sessionID: string) {
|
|
104
|
+
layout.projects.open(directory);
|
|
105
|
+
server.projects.touch(directory);
|
|
106
|
+
navigate(`/${base64Encode(directory)}/session/${sessionID}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function archiveSession(directory: string, sessionID: string) {
|
|
110
|
+
const [, setStore] = sync.child(directory, { bootstrap: false });
|
|
111
|
+
await globalSDK.client.session.update({
|
|
112
|
+
sessionID,
|
|
113
|
+
time: { archived: Date.now() },
|
|
114
|
+
});
|
|
115
|
+
setStore(
|
|
116
|
+
"session",
|
|
117
|
+
(sessions) => sessions.filter((session) => session.id !== sessionID),
|
|
118
|
+
);
|
|
44
119
|
}
|
|
45
120
|
|
|
46
121
|
async function chooseProject() {
|
|
47
122
|
function resolve(result: string | string[] | null) {
|
|
48
123
|
if (Array.isArray(result)) {
|
|
49
124
|
for (const directory of result) {
|
|
50
|
-
openProject(directory)
|
|
125
|
+
openProject(directory);
|
|
51
126
|
}
|
|
52
127
|
} else if (result) {
|
|
53
|
-
openProject(result)
|
|
128
|
+
openProject(result);
|
|
54
129
|
}
|
|
55
130
|
}
|
|
56
131
|
|
|
@@ -58,74 +133,199 @@ export default function Home() {
|
|
|
58
133
|
const result = await platform.openDirectoryPickerDialog?.({
|
|
59
134
|
title: language.t("command.project.open"),
|
|
60
135
|
multiple: true,
|
|
61
|
-
})
|
|
62
|
-
resolve(result)
|
|
136
|
+
});
|
|
137
|
+
resolve(result);
|
|
63
138
|
} else {
|
|
64
139
|
dialog.show(
|
|
65
140
|
() => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
|
|
66
141
|
() => resolve(null),
|
|
67
|
-
)
|
|
142
|
+
);
|
|
68
143
|
}
|
|
69
144
|
}
|
|
70
145
|
|
|
71
146
|
return (
|
|
72
|
-
<div class="mx-auto
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
147
|
+
<div class="mx-auto w-full max-w-7xl px-4 py-6 md:px-6 md:py-8">
|
|
148
|
+
<div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
|
149
|
+
<div class="min-w-0">
|
|
150
|
+
<div class="flex items-center gap-3">
|
|
151
|
+
<div class="flex size-10 items-center justify-center rounded-2xl border border-border-weak bg-background-base/80">
|
|
152
|
+
<Logo class="w-6 opacity-90" />
|
|
153
|
+
</div>
|
|
154
|
+
<div>
|
|
155
|
+
<div class="text-18-medium text-text-strong">
|
|
156
|
+
{language.t("home.sessionBoard")}
|
|
157
|
+
</div>
|
|
158
|
+
<div class="mt-0.5 text-12-regular text-text-weak">
|
|
159
|
+
{language.t("home.sessionBoard.description")}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<div class="flex items-center gap-2 self-start md:justify-end">
|
|
166
|
+
<Button
|
|
167
|
+
icon="folder-add-left"
|
|
168
|
+
size="normal"
|
|
169
|
+
class="pl-2 pr-3"
|
|
170
|
+
onClick={chooseProject}
|
|
171
|
+
>
|
|
172
|
+
{language.t("command.project.open")}
|
|
173
|
+
</Button>
|
|
174
|
+
<Button
|
|
175
|
+
size="normal"
|
|
176
|
+
variant="ghost"
|
|
177
|
+
class="pr-3 text-13-regular text-text-weak"
|
|
178
|
+
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
|
179
|
+
>
|
|
180
|
+
<div
|
|
181
|
+
classList={{
|
|
182
|
+
"size-2 rounded-full": true,
|
|
183
|
+
[serverDotClass()]: true,
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
{server.name}
|
|
187
|
+
</Button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
88
191
|
<Switch>
|
|
89
192
|
<Match when={sync.data.project.length > 0}>
|
|
90
|
-
<div class="mt-
|
|
91
|
-
<
|
|
92
|
-
<div class="
|
|
93
|
-
|
|
193
|
+
<div class="mt-6 flex w-full flex-col gap-6">
|
|
194
|
+
<section class="flex flex-col gap-4">
|
|
195
|
+
<div class="grid gap-3 lg:grid-cols-2">
|
|
196
|
+
<BoardColumnView
|
|
197
|
+
title={language.t("home.sessionBoard.progress")}
|
|
198
|
+
icon="brain"
|
|
199
|
+
tone="progress"
|
|
200
|
+
cards={boardColumns().progress}
|
|
201
|
+
onOpen={openSession}
|
|
202
|
+
onArchive={archiveSession}
|
|
203
|
+
empty={language.t("home.sessionBoard.emptyProgress")}
|
|
204
|
+
/>
|
|
205
|
+
<BoardColumnView
|
|
206
|
+
title={language.t("home.sessionBoard.idle")}
|
|
207
|
+
icon="dash"
|
|
208
|
+
tone="idle"
|
|
209
|
+
cards={boardColumns().idle}
|
|
210
|
+
onOpen={openSession}
|
|
211
|
+
onArchive={archiveSession}
|
|
212
|
+
empty={language.t("home.sessionBoard.emptyIdle")}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
</section>
|
|
216
|
+
</div>
|
|
217
|
+
</Match>
|
|
218
|
+
<Match when={true}>
|
|
219
|
+
<div class="mt-6 rounded-2xl border border-dashed border-border-weak bg-background-base/60 px-6 py-10">
|
|
220
|
+
<div class="mx-auto flex max-w-md flex-col items-center gap-3 text-center">
|
|
221
|
+
<div class="flex size-12 items-center justify-center rounded-2xl border border-border-weak bg-background-base/80">
|
|
222
|
+
<Icon name="folder-add-left" size="large" />
|
|
223
|
+
</div>
|
|
224
|
+
<div class="flex flex-col gap-1 items-center justify-center">
|
|
225
|
+
<div class="text-14-medium text-text-strong">
|
|
226
|
+
{language.t("home.empty.title")}
|
|
227
|
+
</div>
|
|
228
|
+
<div class="text-12-regular text-text-weak">
|
|
229
|
+
{language.t("home.empty.description")}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<Button class="mt-1 px-3" onClick={chooseProject}>
|
|
94
233
|
{language.t("command.project.open")}
|
|
95
234
|
</Button>
|
|
96
235
|
</div>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
236
|
+
</div>
|
|
237
|
+
</Match>
|
|
238
|
+
</Switch>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function BoardColumnView(props: {
|
|
244
|
+
title: string;
|
|
245
|
+
icon: "brain" | "dash";
|
|
246
|
+
tone: BoardColumn;
|
|
247
|
+
cards: BoardCard[];
|
|
248
|
+
empty: string;
|
|
249
|
+
onOpen: (directory: string, sessionID: string) => void;
|
|
250
|
+
onArchive: (directory: string, sessionID: string) => void | Promise<void>;
|
|
251
|
+
}) {
|
|
252
|
+
const toneClass = () => {
|
|
253
|
+
if (props.tone === "progress")
|
|
254
|
+
return "bg-emerald-500/12 text-emerald-300 border-emerald-500/20";
|
|
255
|
+
return "bg-sky-500/12 text-sky-300 border-sky-500/20";
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<section class="rounded-2xl border border-border-weak bg-background-base/80 p-3 md:p-4 min-h-72">
|
|
260
|
+
<div class="flex items-center justify-between gap-3">
|
|
261
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
262
|
+
<div
|
|
263
|
+
class={`size-7 rounded-full border flex items-center justify-center ${toneClass()}`}
|
|
264
|
+
>
|
|
265
|
+
<Icon name={props.icon} size="small" />
|
|
266
|
+
</div>
|
|
267
|
+
<div class="text-14-medium text-text-strong">{props.title}</div>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="text-12-mono text-text-weak">{props.cards.length}</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div class="mt-3 flex flex-col gap-2">
|
|
273
|
+
<Switch>
|
|
274
|
+
<Match when={props.cards.length > 0}>
|
|
275
|
+
<For each={props.cards}>
|
|
276
|
+
{(card) => (
|
|
277
|
+
<div class="overflow-hidden rounded-xl border border-border-weak bg-background-base/70 hover:border-border-strong">
|
|
278
|
+
<div class="flex items-start justify-between gap-3 px-3 pt-3">
|
|
279
|
+
<div class="min-w-0 flex-1 pt-1">
|
|
280
|
+
<div class="truncate text-14-medium leading-tight text-text-strong">
|
|
281
|
+
{card.session.title}
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
<IconButton
|
|
285
|
+
icon="archive"
|
|
286
|
+
variant="ghost"
|
|
287
|
+
class="size-7 rounded-md"
|
|
288
|
+
aria-label="Archive session"
|
|
289
|
+
onClick={(event) => {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
event.stopPropagation();
|
|
292
|
+
void props.onArchive(card.projectDirectory, card.session.id);
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
100
296
|
<Button
|
|
101
|
-
size="large"
|
|
102
297
|
variant="ghost"
|
|
103
|
-
class="
|
|
104
|
-
onClick={() =>
|
|
298
|
+
class="flex h-auto w-full min-w-0 items-center justify-between gap-3 rounded-none border-0 bg-transparent px-3 pb-3 pt-2 text-left"
|
|
299
|
+
onClick={() =>
|
|
300
|
+
props.onOpen(card.projectDirectory, card.session.id)
|
|
301
|
+
}
|
|
105
302
|
>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
303
|
+
<span class="min-w-0 flex-1 truncate text-12-regular text-text-weak">
|
|
304
|
+
{card.projectDirectory.replace(
|
|
305
|
+
/^.*?([^/\\]+(?:[/\\][^/\\]+)?)$/,
|
|
306
|
+
"$1",
|
|
307
|
+
)}
|
|
308
|
+
</span>
|
|
309
|
+
<div class="flex shrink-0 items-center gap-2 text-12-regular text-text-weak">
|
|
310
|
+
<span>{DateTime.fromMillis(card.updatedAt).toRelative()}</span>
|
|
311
|
+
<Icon
|
|
312
|
+
name="arrow-right"
|
|
313
|
+
size="small"
|
|
314
|
+
class="text-icon-weak shrink-0"
|
|
315
|
+
/>
|
|
109
316
|
</div>
|
|
110
317
|
</Button>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
</
|
|
114
|
-
</
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<Icon name="folder-add-left" size="large" />
|
|
119
|
-
<div class="flex flex-col gap-1 items-center justify-center">
|
|
120
|
-
<div class="text-14-medium text-text-strong">{language.t("home.empty.title")}</div>
|
|
121
|
-
<div class="text-12-regular text-text-weak">{language.t("home.empty.description")}</div>
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
</For>
|
|
321
|
+
</Match>
|
|
322
|
+
<Match when={true}>
|
|
323
|
+
<div class="rounded-xl border border-dashed border-border-weak px-3 py-8 text-center text-12-regular text-text-weak">
|
|
324
|
+
{props.empty}
|
|
122
325
|
</div>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</Switch>
|
|
129
|
-
</div>
|
|
130
|
-
)
|
|
326
|
+
</Match>
|
|
327
|
+
</Switch>
|
|
328
|
+
</div>
|
|
329
|
+
</section>
|
|
330
|
+
);
|
|
131
331
|
}
|
package/src/pages/layout.tsx
CHANGED
|
@@ -62,7 +62,6 @@ import {
|
|
|
62
62
|
effectiveWorkspaceOrder,
|
|
63
63
|
errorMessage,
|
|
64
64
|
getDraggableId,
|
|
65
|
-
latestRootSession,
|
|
66
65
|
sortedRootSessions,
|
|
67
66
|
workspaceKey,
|
|
68
67
|
} from "./layout/helpers"
|
|
@@ -1055,69 +1054,7 @@ export default function Layout(props: ParentProps) {
|
|
|
1055
1054
|
if (!directory) return
|
|
1056
1055
|
const root = projectRoot(directory)
|
|
1057
1056
|
server.projects.touch(root)
|
|
1058
|
-
|
|
1059
|
-
let dirs = project
|
|
1060
|
-
? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
|
|
1061
|
-
: [root]
|
|
1062
|
-
const canOpen = (value: string | undefined) => {
|
|
1063
|
-
if (!value) return false
|
|
1064
|
-
return dirs.some((item) => workspaceKey(item) === workspaceKey(value))
|
|
1065
|
-
}
|
|
1066
|
-
const refreshDirs = async (target?: string) => {
|
|
1067
|
-
if (!target || target === root || canOpen(target)) return canOpen(target)
|
|
1068
|
-
const listed = await globalSDK.client.worktree
|
|
1069
|
-
.list({ directory: root })
|
|
1070
|
-
.then((x) => x.data ?? [])
|
|
1071
|
-
.catch(() => [] as string[])
|
|
1072
|
-
dirs = effectiveWorkspaceOrder(root, [root, ...listed], store.workspaceOrder[root])
|
|
1073
|
-
return canOpen(target)
|
|
1074
|
-
}
|
|
1075
|
-
const openSession = async (target: { directory: string; id: string }) => {
|
|
1076
|
-
if (!canOpen(target.directory)) return false
|
|
1077
|
-
const resolved = await globalSDK.client.session
|
|
1078
|
-
.get({ sessionID: target.id })
|
|
1079
|
-
.then((x) => x.data)
|
|
1080
|
-
.catch(() => undefined)
|
|
1081
|
-
if (!resolved?.directory) return false
|
|
1082
|
-
if (!canOpen(resolved.directory)) return false
|
|
1083
|
-
setStore("lastProjectSession", root, { directory: resolved.directory, id: resolved.id, at: Date.now() })
|
|
1084
|
-
navigateWithSidebarReset(`/${base64Encode(resolved.directory)}/session/${resolved.id}`)
|
|
1085
|
-
return true
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
const projectSession = store.lastProjectSession[root]
|
|
1089
|
-
if (projectSession?.id) {
|
|
1090
|
-
await refreshDirs(projectSession.directory)
|
|
1091
|
-
const opened = await openSession(projectSession)
|
|
1092
|
-
if (opened) return
|
|
1093
|
-
clearLastProjectSession(root)
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const latest = latestRootSession(
|
|
1097
|
-
dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]),
|
|
1098
|
-
Date.now(),
|
|
1099
|
-
)
|
|
1100
|
-
if (latest && (await openSession(latest))) {
|
|
1101
|
-
return
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
const fetched = latestRootSession(
|
|
1105
|
-
await Promise.all(
|
|
1106
|
-
dirs.map(async (item) => ({
|
|
1107
|
-
path: { directory: item },
|
|
1108
|
-
session: await globalSDK.client.session
|
|
1109
|
-
.list({ directory: item })
|
|
1110
|
-
.then((x) => x.data ?? [])
|
|
1111
|
-
.catch(() => []),
|
|
1112
|
-
})),
|
|
1113
|
-
),
|
|
1114
|
-
Date.now(),
|
|
1115
|
-
)
|
|
1116
|
-
if (fetched && (await openSession(fetched))) {
|
|
1117
|
-
return
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
navigateWithSidebarReset(`/${base64Encode(root)}/session`)
|
|
1057
|
+
navigateWithSidebarReset(`/${base64Encode(root)}`)
|
|
1121
1058
|
}
|
|
1122
1059
|
|
|
1123
1060
|
function navigateToSession(session: Session | undefined) {
|
|
@@ -190,6 +190,7 @@ export function MessageTimeline(props: {
|
|
|
190
190
|
setScrollRef: (el: HTMLDivElement | undefined) => void
|
|
191
191
|
onScheduleScrollState: (el: HTMLDivElement) => void
|
|
192
192
|
onAutoScrollHandleScroll: () => void
|
|
193
|
+
isUserScrolled: () => boolean
|
|
193
194
|
onMarkScrollGesture: (target?: EventTarget | null) => void
|
|
194
195
|
hasScrollGesture: () => boolean
|
|
195
196
|
isDesktop: boolean
|
|
@@ -538,8 +539,8 @@ export function MessageTimeline(props: {
|
|
|
538
539
|
onScroll={(e) => {
|
|
539
540
|
props.onScheduleScrollState(e.currentTarget)
|
|
540
541
|
props.onTurnBackfillScroll()
|
|
541
|
-
if (!props.hasScrollGesture()) return
|
|
542
542
|
props.onAutoScrollHandleScroll()
|
|
543
|
+
if (!props.hasScrollGesture() && !props.isUserScrolled()) return
|
|
543
544
|
props.onMarkScrollGesture(e.currentTarget)
|
|
544
545
|
if (props.isDesktop) props.onScrollSpyScroll()
|
|
545
546
|
}}
|
|
@@ -20,6 +20,8 @@ export interface SessionReviewTabProps {
|
|
|
20
20
|
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
|
21
21
|
diffStyle: DiffStyle
|
|
22
22
|
onDiffStyleChange?: (style: DiffStyle) => void
|
|
23
|
+
wordWrap?: boolean
|
|
24
|
+
onWordWrapChange?: (value: boolean) => void
|
|
23
25
|
onViewFile?: (file: string) => void
|
|
24
26
|
onRevealFile?: (file: string) => void
|
|
25
27
|
onLineComment?: (comment: { file: string; selection: SelectedLineRange; comment: string; preview?: string }) => void
|
|
@@ -164,6 +166,8 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
|
|
|
164
166
|
diffs={props.diffs()}
|
|
165
167
|
diffStyle={props.diffStyle}
|
|
166
168
|
onDiffStyleChange={props.onDiffStyleChange}
|
|
169
|
+
wordWrap={props.wordWrap}
|
|
170
|
+
onWordWrapChange={props.onWordWrapChange}
|
|
167
171
|
onViewFile={props.onViewFile}
|
|
168
172
|
onRevealFile={props.onRevealFile}
|
|
169
173
|
focusedFile={props.focusedFile}
|
package/src/pages/session.tsx
CHANGED
|
@@ -775,6 +775,8 @@ export default function Page() {
|
|
|
775
775
|
const reviewContent = (input: {
|
|
776
776
|
diffStyle: DiffStyle
|
|
777
777
|
onDiffStyleChange?: (style: DiffStyle) => void
|
|
778
|
+
wordWrap: boolean
|
|
779
|
+
onWordWrapChange?: (value: boolean) => void
|
|
778
780
|
classes?: SessionReviewTabProps["classes"]
|
|
779
781
|
loadingClass: string
|
|
780
782
|
emptyClass: string
|
|
@@ -793,6 +795,8 @@ export default function Page() {
|
|
|
793
795
|
view={view}
|
|
794
796
|
diffStyle={input.diffStyle}
|
|
795
797
|
onDiffStyleChange={input.onDiffStyleChange}
|
|
798
|
+
wordWrap={input.wordWrap}
|
|
799
|
+
onWordWrapChange={input.onWordWrapChange}
|
|
796
800
|
onScrollRef={(el) => setTree("reviewScroll", el)}
|
|
797
801
|
focusedFile={tree.activeDiff}
|
|
798
802
|
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
|
@@ -821,6 +825,8 @@ export default function Page() {
|
|
|
821
825
|
view={view}
|
|
822
826
|
diffStyle={input.diffStyle}
|
|
823
827
|
onDiffStyleChange={input.onDiffStyleChange}
|
|
828
|
+
wordWrap={input.wordWrap}
|
|
829
|
+
onWordWrapChange={input.onWordWrapChange}
|
|
824
830
|
onScrollRef={(el) => setTree("reviewScroll", el)}
|
|
825
831
|
focusedFile={tree.activeDiff}
|
|
826
832
|
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
|
@@ -852,6 +858,8 @@ export default function Page() {
|
|
|
852
858
|
view={view}
|
|
853
859
|
diffStyle={input.diffStyle}
|
|
854
860
|
onDiffStyleChange={input.onDiffStyleChange}
|
|
861
|
+
wordWrap={input.wordWrap}
|
|
862
|
+
onWordWrapChange={input.onWordWrapChange}
|
|
855
863
|
onScrollRef={(el) => setTree("reviewScroll", el)}
|
|
856
864
|
focusedFile={tree.activeDiff}
|
|
857
865
|
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
|
|
@@ -875,6 +883,8 @@ export default function Page() {
|
|
|
875
883
|
{reviewContent({
|
|
876
884
|
diffStyle: layout.review.diffStyle(),
|
|
877
885
|
onDiffStyleChange: layout.review.setDiffStyle,
|
|
886
|
+
wordWrap: layout.review.wordWrap(),
|
|
887
|
+
onWordWrapChange: layout.review.setWordWrap,
|
|
878
888
|
loadingClass: "px-6 py-4 text-text-weak",
|
|
879
889
|
emptyClass: "h-full pb-30 flex flex-col items-center justify-center text-center gap-6",
|
|
880
890
|
})}
|
|
@@ -1251,6 +1261,8 @@ export default function Page() {
|
|
|
1251
1261
|
mobileChanges={mobileChanges()}
|
|
1252
1262
|
mobileFallback={reviewContent({
|
|
1253
1263
|
diffStyle: "unified",
|
|
1264
|
+
wordWrap: layout.review.wordWrap(),
|
|
1265
|
+
onWordWrapChange: layout.review.setWordWrap,
|
|
1254
1266
|
classes: {
|
|
1255
1267
|
root: "pb-8",
|
|
1256
1268
|
header: "px-4",
|
|
@@ -1264,6 +1276,7 @@ export default function Page() {
|
|
|
1264
1276
|
setScrollRef={setScrollRef}
|
|
1265
1277
|
onScheduleScrollState={scheduleScrollState}
|
|
1266
1278
|
onAutoScrollHandleScroll={autoScroll.handleScroll}
|
|
1279
|
+
isUserScrolled={autoScroll.userScrolled}
|
|
1267
1280
|
onMarkScrollGesture={markScrollGesture}
|
|
1268
1281
|
hasScrollGesture={hasScrollGesture}
|
|
1269
1282
|
isDesktop={isDesktop()}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{u as D,a as C,b as S,c as L,d as P,e as z,f as M,g,i as l,h as a,L as A,j as B,k as E,m as w,D as F,B as u,M as y,F as I,l as R,I as G,S as H,n as N,o as T,t as o}from"./index-BR3vRMWX.js";var q=o("<div>"),J=o('<div class="mt-20 w-full flex flex-col gap-4"><div class="flex gap-2 items-center justify-between pl-3"><div class="text-14-medium text-text-strong"></div></div><ul class="flex flex-col gap-2">'),K=o('<div class="mt-30 mx-auto flex flex-col items-center gap-3"><div class="flex flex-col gap-1 items-center justify-center"><div class="text-14-medium text-text-strong"></div><div class="text-12-regular text-text-weak">'),O=o('<div class="mx-auto mt-55 w-full md:w-auto px-4">'),Q=o('<div class="text-14-regular text-text-weak">');function V(){const m=D(),$=C(),p=S(),x=L(),_=P(),n=z(),s=M(),b=g(()=>m.data.path.home),k=g(()=>m.data.project.slice().sort((t,e)=>(e.time.updated??e.time.created)-(t.time.updated??t.time.created)).slice(0,5)),j=g(()=>{const t=n.healthy();return t===!0?"bg-icon-success-base":t===!1?"bg-icon-critical-base":"bg-border-weak-base"});function d(t){$.projects.open(t),n.projects.touch(t),_(`/${N(t)}`)}async function h(){function t(e){if(Array.isArray(e))for(const r of e)d(r);else e&&d(e)}if(p.openDirectoryPickerDialog&&n.isLocal()){const e=await p.openDirectoryPickerDialog?.({title:s.t("command.project.open"),multiple:!0});t(e)}else x.show(()=>a(T,{multiple:!0,onSelect:t}),()=>t(null))}return(()=>{var t=O();return l(t,a(A,{class:"md:w-xl opacity-12"}),null),l(t,a(u,{size:"large",variant:"ghost",class:"mt-4 mx-auto text-14-regular text-text-weak",onClick:()=>x.show(()=>a(F,{})),get children(){return[(()=>{var e=q();return B(r=>E(e,{"size-2 rounded-full":!0,[j()]:!0},r)),e})(),w(()=>n.name)]}}),null),l(t,a(H,{get children(){return[a(y,{get when(){return m.data.project.length>0},get children(){var e=J(),r=e.firstChild,i=r.firstChild,f=r.nextSibling;return l(i,()=>s.t("home.recentProjects")),l(r,a(u,{icon:"folder-add-left",size:"normal",class:"pl-2 pr-3",onClick:h,get children(){return s.t("command.project.open")}}),null),l(f,a(I,{get each(){return k()},children:c=>a(u,{size:"large",variant:"ghost",class:"text-14-mono text-left justify-between px-3",onClick:()=>d(c.worktree),get children(){return[w(()=>c.worktree.replace(b(),"~")),(()=>{var v=Q();return l(v,()=>R.fromMillis(c.time.updated??c.time.created).toRelative()),v})()]}})})),e}}),a(y,{when:!0,get children(){var e=K(),r=e.firstChild,i=r.firstChild,f=i.nextSibling;return l(e,a(G,{name:"folder-add-left",size:"large"}),r),l(i,()=>s.t("home.empty.title")),l(f,()=>s.t("home.empty.description")),l(e,a(u,{class:"px-3 mt-1",onClick:h,get children(){return s.t("command.project.open")}}),null),e}})]}}),null),t})()}export{V as default};
|