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.
@@ -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 { useServer } from "@/context/server"
14
- import { useGlobalSync } from "@/context/global-sync"
15
- import { useLanguage } from "@/context/language"
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 server = useServer()
24
- const language = useLanguage()
25
- const homedir = createMemo(() => sync.data.path.home)
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((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
30
- .slice(0, 5)
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 mt-55 w-full md:w-auto px-4">
73
- <Logo class="md:w-xl opacity-12" />
74
- <Button
75
- size="large"
76
- variant="ghost"
77
- class="mt-4 mx-auto text-14-regular text-text-weak"
78
- onClick={() => dialog.show(() => <DialogSelectServer />)}
79
- >
80
- <div
81
- classList={{
82
- "size-2 rounded-full": true,
83
- [serverDotClass()]: true,
84
- }}
85
- />
86
- {server.name}
87
- </Button>
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-20 w-full flex flex-col gap-4">
91
- <div class="flex gap-2 items-center justify-between pl-3">
92
- <div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div>
93
- <Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
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
- <ul class="flex flex-col gap-2">
98
- <For each={recent()}>
99
- {(project) => (
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="text-14-mono text-left justify-between px-3"
104
- onClick={() => openProject(project.worktree)}
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
- {project.worktree.replace(homedir(), "~")}
107
- <div class="text-14-regular text-text-weak">
108
- {DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
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
- </For>
113
- </ul>
114
- </div>
115
- </Match>
116
- <Match when={true}>
117
- <div class="mt-30 mx-auto flex flex-col items-center gap-3">
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
- <Button class="px-3 mt-1" onClick={chooseProject}>
124
- {language.t("command.project.open")}
125
- </Button>
126
- </div>
127
- </Match>
128
- </Switch>
129
- </div>
130
- )
326
+ </Match>
327
+ </Switch>
328
+ </div>
329
+ </section>
330
+ );
131
331
  }
@@ -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
- const project = layout.projects.list().find((item) => item.worktree === root)
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}
@@ -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};