ikanban-web 0.2.7 → 0.2.9

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.
@@ -279,59 +279,6 @@ export default function Layout(props: ParentProps) {
279
279
  setLocale(next)
280
280
  }
281
281
 
282
- const useUpdatePolling = () =>
283
- onMount(() => {
284
- if (!platform.checkUpdate || !platform.update || !platform.restart) return
285
-
286
- let toastId: number | undefined
287
- let interval: ReturnType<typeof setInterval> | undefined
288
-
289
- const pollUpdate = () =>
290
- platform.checkUpdate!().then(({ updateAvailable, version }) => {
291
- if (!updateAvailable) return
292
- if (toastId !== undefined) return
293
- toastId = showToast({
294
- persistent: true,
295
- icon: "download",
296
- title: language.t("toast.update.title"),
297
- description: language.t("toast.update.description", { version: version ?? "" }),
298
- actions: [
299
- {
300
- label: language.t("toast.update.action.installRestart"),
301
- onClick: async () => {
302
- await platform.update!()
303
- await platform.restart!()
304
- },
305
- },
306
- {
307
- label: language.t("toast.update.action.notYet"),
308
- onClick: "dismiss",
309
- },
310
- ],
311
- })
312
- })
313
-
314
- createEffect(() => {
315
- if (!settings.ready()) return
316
-
317
- if (!settings.updates.startup()) {
318
- if (interval === undefined) return
319
- clearInterval(interval)
320
- interval = undefined
321
- return
322
- }
323
-
324
- if (interval !== undefined) return
325
- void pollUpdate()
326
- interval = setInterval(pollUpdate, 10 * 60 * 1000)
327
- })
328
-
329
- onCleanup(() => {
330
- if (interval === undefined) return
331
- clearInterval(interval)
332
- })
333
- })
334
-
335
282
  const useSDKNotificationToasts = () =>
336
283
  onMount(() => {
337
284
  const toastBySession = new Map<string, number>()
@@ -440,7 +387,6 @@ export default function Layout(props: ParentProps) {
440
387
  })
441
388
  })
442
389
 
443
- useUpdatePolling()
444
390
  useSDKNotificationToasts()
445
391
 
446
392
  function scrollToSession(sessionId: string, sessionKey: string) {
@@ -871,6 +817,17 @@ export default function Layout(props: ParentProps) {
871
817
  keybind: "mod+o",
872
818
  onSelect: () => chooseProject(),
873
819
  },
820
+ {
821
+ id: "project.close",
822
+ title: language.t("command.project.close"),
823
+ category: language.t("command.category.project"),
824
+ disabled: !currentProject(),
825
+ onSelect: () => {
826
+ const project = currentProject()
827
+ if (!project) return
828
+ closeProject(project.worktree)
829
+ },
830
+ },
874
831
  {
875
832
  id: "provider.connect",
876
833
  title: language.t("command.provider.connect"),
@@ -20,7 +20,10 @@ 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
26
+ onRevealFile?: (file: string) => void
24
27
  onLineComment?: (comment: { file: string; selection: SelectedLineRange; comment: string; preview?: string }) => void
25
28
  onLineCommentUpdate?: (comment: SessionReviewCommentUpdate) => void
26
29
  onLineCommentDelete?: (comment: SessionReviewCommentDelete) => void
@@ -56,7 +59,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
56
59
 
57
60
  const readFile = async (path: string) => {
58
61
  return sdk.client.file
59
- .read({ path })
62
+ .read({ path, directory: sdk.directory })
60
63
  .then((x) => x.data)
61
64
  .catch((error) => {
62
65
  console.debug("[session-review] failed to read file", { path, error })
@@ -163,7 +166,10 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
163
166
  diffs={props.diffs()}
164
167
  diffStyle={props.diffStyle}
165
168
  onDiffStyleChange={props.onDiffStyleChange}
169
+ wordWrap={props.wordWrap}
170
+ onWordWrapChange={props.onWordWrapChange}
166
171
  onViewFile={props.onViewFile}
172
+ onRevealFile={props.onRevealFile}
167
173
  focusedFile={props.focusedFile}
168
174
  readFile={readFile}
169
175
  onLineComment={props.onLineComment}
@@ -20,7 +20,6 @@ import { useCommand } from "@/context/command"
20
20
  import { useFile, type SelectedLineRange } from "@/context/file"
21
21
  import { useLanguage } from "@/context/language"
22
22
  import { useLayout } from "@/context/layout"
23
- import { useSync } from "@/context/sync"
24
23
  import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
25
24
  import { FileTabContent } from "@/pages/session/file-tabs"
26
25
  import { createOpenSessionFileTab, getTabReorderIndex } from "@/pages/session/helpers"
@@ -31,10 +30,15 @@ export function SessionSidePanel(props: {
31
30
  reviewPanel: () => JSX.Element
32
31
  activeDiff?: string
33
32
  focusReviewDiff: (path: string) => void
33
+ showReview: () => boolean
34
+ reviewCount: () => number
35
+ hasReview: () => boolean
36
+ diffsReady: () => boolean
37
+ diffFiles: () => string[]
38
+ kinds: () => Map<string, "add" | "del" | "mix">
34
39
  }) {
35
40
  const params = useParams()
36
41
  const layout = useLayout()
37
- const sync = useSync()
38
42
  const file = useFile()
39
43
  const language = useLanguage()
40
44
  const command = useCommand()
@@ -49,44 +53,6 @@ export function SessionSidePanel(props: {
49
53
  const open = createMemo(() => isDesktop() && (view().reviewPanel.opened() || layout.fileTree.opened()))
50
54
  const reviewTab = createMemo(() => isDesktop())
51
55
 
52
- const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
53
- const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
54
- const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
55
- const hasReview = createMemo(() => reviewCount() > 0)
56
- const diffsReady = createMemo(() => {
57
- const id = params.id
58
- if (!id) return true
59
- if (!hasReview()) return true
60
- return sync.data.session_diff[id] !== undefined
61
- })
62
-
63
- const diffFiles = createMemo(() => diffs().map((d) => d.file))
64
- const kinds = createMemo(() => {
65
- const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
66
- if (!a) return b
67
- if (a === b) return a
68
- return "mix" as const
69
- }
70
-
71
- const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
72
-
73
- const out = new Map<string, "add" | "del" | "mix">()
74
- for (const diff of diffs()) {
75
- const file = normalize(diff.file)
76
- const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
77
-
78
- out.set(file, kind)
79
-
80
- const parts = file.split("/")
81
- for (const [idx] of parts.slice(0, -1).entries()) {
82
- const dir = parts.slice(0, idx + 1).join("/")
83
- if (!dir) continue
84
- out.set(dir, merge(out.get(dir), kind))
85
- }
86
- }
87
- return out
88
- })
89
-
90
56
  const normalizeTab = (tab: string) => {
91
57
  if (!tab.startsWith("file://")) return tab
92
58
  return file.tab(tab)
@@ -121,7 +87,7 @@ export function SessionSidePanel(props: {
121
87
  const first = openedTabs()[0]
122
88
  if (first) return first
123
89
  if (contextOpen()) return "context"
124
- if (reviewTab() && hasReview()) return "review"
90
+ if (reviewTab() && props.showReview()) return "review"
125
91
  return "empty"
126
92
  })
127
93
 
@@ -236,8 +202,8 @@ export function SessionSidePanel(props: {
236
202
  <Tabs.Trigger value="review">
237
203
  <div class="flex items-center gap-1.5">
238
204
  <div>{language.t("session.tab.review")}</div>
239
- <Show when={hasReview()}>
240
- <div>{reviewCount()}</div>
205
+ <Show when={props.hasReview()}>
206
+ <div>{props.reviewCount()}</div>
241
207
  </Show>
242
208
  </div>
243
209
  </Tabs.Trigger>
@@ -356,8 +322,8 @@ export function SessionSidePanel(props: {
356
322
  >
357
323
  <Tabs.List data-scrolled={store.fileTreeScrolled ? "" : undefined}>
358
324
  <Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
359
- {reviewCount()}{" "}
360
- {language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")}
325
+ {props.reviewCount()}{" "}
326
+ {language.t(props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")}
361
327
  </Tabs.Trigger>
362
328
  <Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
363
329
  {language.t("session.files.all")}
@@ -370,9 +336,9 @@ export function SessionSidePanel(props: {
370
336
  class="bg-background-stronger px-3 py-0"
371
337
  >
372
338
  <Switch>
373
- <Match when={hasReview()}>
339
+ <Match when={props.hasReview()}>
374
340
  <Show
375
- when={diffsReady()}
341
+ when={props.diffsReady()}
376
342
  fallback={
377
343
  <div class="px-2 py-2 text-12-regular text-text-weak">
378
344
  {language.t("common.loading")}
@@ -382,8 +348,8 @@ export function SessionSidePanel(props: {
382
348
  >
383
349
  <FileTree
384
350
  path=""
385
- allowed={diffFiles()}
386
- kinds={kinds()}
351
+ allowed={props.diffFiles()}
352
+ kinds={props.kinds()}
387
353
  draggable={false}
388
354
  active={props.activeDiff}
389
355
  onFileClick={(node) => props.focusReviewDiff(node.path)}
@@ -405,8 +371,8 @@ export function SessionSidePanel(props: {
405
371
  >
406
372
  <FileTree
407
373
  path=""
408
- modified={diffFiles()}
409
- kinds={kinds()}
374
+ modified={props.diffFiles()}
375
+ kinds={props.kinds()}
410
376
  onFileClick={(node) => openTab(file.tab(node.path))}
411
377
  />
412
378
  </Tabs.Content>
@@ -1,4 +1,5 @@
1
1
  import { createMemo } from "solid-js";
2
+ import { produce } from "solid-js/store";
2
3
  import { useNavigate, useParams } from "@solidjs/router";
3
4
  import { useCommand, type CommandOption } from "@/context/command";
4
5
  import { useDialog } from "ikanban-ui/context/dialog";
@@ -20,6 +21,7 @@ import { DialogSelectFile } from "@/components/dialog-select-file";
20
21
  import { DialogSelectModel } from "@/components/dialog-select-model";
21
22
  import { DialogSelectMcp } from "@/components/dialog-select-mcp";
22
23
  import { DialogFork } from "@/components/dialog-fork";
24
+ import { DialogSessionTimeline } from "@/components/dialog-session-timeline";
23
25
  import { showToast } from "ikanban-ui/toast";
24
26
  import { findLast } from "ikanban-utils/array";
25
27
  import { extractPromptFromParts } from "@/utils/prompt";
@@ -79,6 +81,77 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
79
81
  return userMessages().filter((m) => m.id < revert);
80
82
  });
81
83
 
84
+ const refreshReviewDiffs = async (sessionID: string) => {
85
+ const directory = sdk.directory;
86
+ sync.set(
87
+ produce((draft) => {
88
+ delete draft.session_diff[sessionID];
89
+ delete draft.project_diff[directory];
90
+ }),
91
+ );
92
+ await Promise.allSettled([
93
+ sync.session.diff(sessionID),
94
+ sync.projectDiff.diff(),
95
+ ]);
96
+ };
97
+
98
+ const revertToMessage = async (message: UserMessage) => {
99
+ const sessionID = params.id;
100
+ if (!sessionID) return;
101
+ if (status()?.type !== "idle") {
102
+ await sdk.client.session.abort({ sessionID }).catch(() => { });
103
+ }
104
+ await sdk.client.session.revert({ sessionID, messageID: message.id });
105
+ const parts = sync.data.part[message.id];
106
+ if (parts) {
107
+ const restored = extractPromptFromParts(parts, {
108
+ directory: sdk.directory,
109
+ });
110
+ prompt.set(restored);
111
+ }
112
+ await refreshReviewDiffs(sessionID);
113
+ const priorMessage = findLast(userMessages(), (x) => x.id < message.id);
114
+ setActiveMessage(priorMessage);
115
+ };
116
+
117
+ const redoToMessage = async (message: UserMessage | undefined) => {
118
+ const sessionID = params.id;
119
+ if (!sessionID) return;
120
+ const revertMessageID = info()?.revert?.messageID;
121
+ if (!revertMessageID) return;
122
+ if (!message) {
123
+ await sdk.client.session.unrevert({ sessionID });
124
+ prompt.reset();
125
+ await refreshReviewDiffs(sessionID);
126
+ const lastMsg = findLast(
127
+ userMessages(),
128
+ (x) => x.id >= revertMessageID,
129
+ );
130
+ setActiveMessage(lastMsg);
131
+ return;
132
+ }
133
+ await sdk.client.session.revert({
134
+ sessionID,
135
+ messageID: message.id,
136
+ });
137
+ await refreshReviewDiffs(sessionID);
138
+ const priorMsg = findLast(userMessages(), (x) => x.id < message.id);
139
+ setActiveMessage(priorMsg);
140
+ };
141
+
142
+ const jumpToMessage = async (message: UserMessage | undefined) => {
143
+ const revertMessageID = info()?.revert?.messageID;
144
+ if (!message) {
145
+ await redoToMessage(undefined);
146
+ return;
147
+ }
148
+ if (!revertMessageID || message.id < revertMessageID) {
149
+ await revertToMessage(message);
150
+ return;
151
+ }
152
+ await redoToMessage(message);
153
+ };
154
+
82
155
  const showAllFiles = () => {
83
156
  if (layout.fileTree.tab() !== "changes") return;
84
157
  layout.fileTree.setTab("all");
@@ -317,29 +390,31 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
317
390
  slash: "undo",
318
391
  disabled: !params.id || visibleUserMessages().length === 0,
319
392
  onSelect: async () => {
320
- const sessionID = params.id;
321
- if (!sessionID) return;
322
- if (status()?.type !== "idle") {
323
- await sdk.client.session.abort({ sessionID }).catch(() => { });
324
- }
325
393
  const revert = info()?.revert?.messageID;
326
394
  const message = findLast(
327
395
  userMessages(),
328
396
  (x) => !revert || x.id < revert,
329
397
  );
330
398
  if (!message) return;
331
- await sdk.client.session.revert({ sessionID, messageID: message.id });
332
- const parts = sync.data.part[message.id];
333
- if (parts) {
334
- const restored = extractPromptFromParts(parts, {
335
- directory: sdk.directory,
336
- });
337
- prompt.set(restored);
338
- }
339
- const priorMessage = findLast(userMessages(), (x) => x.id < message.id);
340
- setActiveMessage(priorMessage);
399
+ await revertToMessage(message);
341
400
  },
342
401
  }),
402
+ sessionCommand({
403
+ id: "session.timeline",
404
+ title: language.t("command.session.timeline"),
405
+ description: language.t("command.session.timeline.description"),
406
+ slash: "timeline",
407
+ disabled: !params.id || userMessages().length === 0,
408
+ onSelect: () =>
409
+ dialog.show(() => (
410
+ <DialogSessionTimeline
411
+ onSelect={(message) => {
412
+ dialog.close();
413
+ void jumpToMessage(message);
414
+ }}
415
+ />
416
+ )),
417
+ }),
343
418
  sessionCommand({
344
419
  id: "session.redo",
345
420
  title: language.t("command.session.redo"),
@@ -347,27 +422,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
347
422
  slash: "redo",
348
423
  disabled: !params.id || !info()?.revert?.messageID,
349
424
  onSelect: async () => {
350
- const sessionID = params.id;
351
- if (!sessionID) return;
352
425
  const revertMessageID = info()?.revert?.messageID;
353
426
  if (!revertMessageID) return;
354
427
  const nextMessage = userMessages().find((x) => x.id > revertMessageID);
355
- if (!nextMessage) {
356
- await sdk.client.session.unrevert({ sessionID });
357
- prompt.reset();
358
- const lastMsg = findLast(
359
- userMessages(),
360
- (x) => x.id >= revertMessageID,
361
- );
362
- setActiveMessage(lastMsg);
363
- return;
364
- }
365
- await sdk.client.session.revert({
366
- sessionID,
367
- messageID: nextMessage.id,
368
- });
369
- const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id);
370
- setActiveMessage(priorMsg);
428
+ await redoToMessage(nextMessage);
371
429
  },
372
430
  }),
373
431
  sessionCommand({
@@ -360,9 +360,8 @@ export default function Page() {
360
360
  })
361
361
 
362
362
  const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
363
- const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
364
- const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
365
- const hasReview = createMemo(() => reviewCount() > 0)
363
+ const sessionDiffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
364
+ const projectDiffs = createMemo(() => sync.data.project_diff[sdk.directory] ?? [])
366
365
  const revertMessageID = createMemo(() => info()?.revert?.messageID)
367
366
  const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
368
367
  const messagesReady = createMemo(() => {
@@ -414,7 +413,7 @@ export default function Page() {
414
413
  const [store, setStore] = createStore({
415
414
  messageId: undefined as string | undefined,
416
415
  mobileTab: "session" as "session" | "changes",
417
- changes: "session" as "session" | "turn",
416
+ changes: "project" as "session" | "project" | "turn",
418
417
  newSessionWorktree: "main",
419
418
  deferRender: false,
420
419
  })
@@ -431,7 +430,42 @@ export default function Page() {
431
430
  }, sessionKey())
432
431
 
433
432
  const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
434
- const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs()))
433
+ const reviewDiffs = createMemo(() => {
434
+ if (store.changes === "project") return projectDiffs()
435
+ if (store.changes === "turn") return turnDiffs()
436
+ return sessionDiffs()
437
+ })
438
+ const reviewDiffFiles = createMemo(() => reviewDiffs().map((diff) => diff.file))
439
+ const reviewKinds = createMemo(() => {
440
+ const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
441
+ if (!a) return b
442
+ if (a === b) return a
443
+ return "mix" as const
444
+ }
445
+
446
+ const normalize = (p: string) => p.replaceAll("\\", "/").replace(/\/+$/, "")
447
+
448
+ const out = new Map<string, "add" | "del" | "mix">()
449
+ for (const diff of reviewDiffs()) {
450
+ const file = normalize(diff.file)
451
+ const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
452
+
453
+ out.set(file, kind)
454
+
455
+ const parts = file.split("/")
456
+ for (const [idx] of parts.slice(0, -1).entries()) {
457
+ const dir = parts.slice(0, idx + 1).join("/")
458
+ if (!dir) continue
459
+ out.set(dir, merge(out.get(dir), kind))
460
+ }
461
+ }
462
+ return out
463
+ })
464
+ const reviewCount = createMemo(() => reviewDiffs().length)
465
+ const hasReview = createMemo(() => reviewCount() > 0)
466
+ const hasAnyReview = createMemo(
467
+ () => sessionDiffs().length > 0 || projectDiffs().length > 0 || turnDiffs().length > 0,
468
+ )
435
469
 
436
470
  const newSessionWorktree = createMemo(() => {
437
471
  if (store.newSessionWorktree === "create") return "create"
@@ -471,10 +505,17 @@ export default function Page() {
471
505
  const diffsReady = createMemo(() => {
472
506
  const id = params.id
473
507
  if (!id) return true
508
+ if (store.changes === "turn") return true
474
509
  if (!hasReview()) return true
510
+ if (store.changes === "project") return sync.data.project_diff[sdk.directory] !== undefined
475
511
  return sync.data.session_diff[id] !== undefined
476
512
  })
477
513
  const reviewEmptyKey = createMemo(() => {
514
+ if (store.changes === "project") {
515
+ const project = sync.project
516
+ if (!project || project.vcs) return "session.review.emptyProject"
517
+ return "session.review.noVcsProject"
518
+ }
478
519
  const project = sync.project
479
520
  if (!project || project.vcs) return "session.review.empty"
480
521
  return "session.review.noVcs"
@@ -528,7 +569,7 @@ export default function Page() {
528
569
  sessionKey,
529
570
  () => {
530
571
  setStore("messageId", undefined)
531
- setStore("changes", "session")
572
+ setStore("changes", "project")
532
573
  },
533
574
  { defer: true },
534
575
  ),
@@ -705,16 +746,18 @@ export default function Page() {
705
746
  loadFile: file.load,
706
747
  })
707
748
 
708
- const changesOptions = ["session", "turn"] as const
749
+ const changesOptions = ["project", "session", "turn"] as const
709
750
  const changesOptionsList = [...changesOptions]
710
751
 
711
752
  const changesTitle = () => (
712
753
  <Select
713
754
  options={changesOptionsList}
714
755
  current={store.changes}
715
- label={(option) =>
716
- option === "session" ? language.t("ui.sessionReview.title") : language.t("ui.sessionReview.title.lastTurn")
717
- }
756
+ label={(option) => {
757
+ if (option === "project") return language.t("ui.sessionReview.title.project")
758
+ if (option === "turn") return language.t("ui.sessionReview.title.lastTurn")
759
+ return language.t("ui.sessionReview.title")
760
+ }}
718
761
  onSelect={(option) => option && setStore("changes", option)}
719
762
  variant="ghost"
720
763
  size="small"
@@ -732,6 +775,8 @@ export default function Page() {
732
775
  const reviewContent = (input: {
733
776
  diffStyle: DiffStyle
734
777
  onDiffStyleChange?: (style: DiffStyle) => void
778
+ wordWrap: boolean
779
+ onWordWrapChange?: (value: boolean) => void
735
780
  classes?: SessionReviewTabProps["classes"]
736
781
  loadingClass: string
737
782
  emptyClass: string
@@ -743,9 +788,15 @@ export default function Page() {
743
788
  title={changesTitle()}
744
789
  empty={emptyTurn()}
745
790
  diffs={reviewDiffs}
791
+ onRevealFile={(file) => {
792
+ if (store.changes !== "project") return
793
+ void sync.projectDiff.hydrate(file)
794
+ }}
746
795
  view={view}
747
796
  diffStyle={input.diffStyle}
748
797
  onDiffStyleChange={input.onDiffStyleChange}
798
+ wordWrap={input.wordWrap}
799
+ onWordWrapChange={input.onWordWrapChange}
749
800
  onScrollRef={(el) => setTree("reviewScroll", el)}
750
801
  focusedFile={tree.activeDiff}
751
802
  onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
@@ -767,9 +818,15 @@ export default function Page() {
767
818
  <SessionReviewTab
768
819
  title={changesTitle()}
769
820
  diffs={reviewDiffs}
821
+ onRevealFile={(file) => {
822
+ if (store.changes !== "project") return
823
+ void sync.projectDiff.hydrate(file)
824
+ }}
770
825
  view={view}
771
826
  diffStyle={input.diffStyle}
772
827
  onDiffStyleChange={input.onDiffStyleChange}
828
+ wordWrap={input.wordWrap}
829
+ onWordWrapChange={input.onWordWrapChange}
773
830
  onScrollRef={(el) => setTree("reviewScroll", el)}
774
831
  focusedFile={tree.activeDiff}
775
832
  onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
@@ -801,6 +858,8 @@ export default function Page() {
801
858
  view={view}
802
859
  diffStyle={input.diffStyle}
803
860
  onDiffStyleChange={input.onDiffStyleChange}
861
+ wordWrap={input.wordWrap}
862
+ onWordWrapChange={input.onWordWrapChange}
804
863
  onScrollRef={(el) => setTree("reviewScroll", el)}
805
864
  focusedFile={tree.activeDiff}
806
865
  onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
@@ -824,6 +883,8 @@ export default function Page() {
824
883
  {reviewContent({
825
884
  diffStyle: layout.review.diffStyle(),
826
885
  onDiffStyleChange: layout.review.setDiffStyle,
886
+ wordWrap: layout.review.wordWrap(),
887
+ onWordWrapChange: layout.review.setWordWrap,
827
888
  loadingClass: "px-6 py-4 text-text-weak",
828
889
  emptyClass: "h-full pb-30 flex flex-col items-center justify-center text-center gap-6",
829
890
  })}
@@ -935,14 +996,14 @@ export default function Page() {
935
996
  const first = openedTabs()[0]
936
997
  if (first) return first
937
998
  if (contextOpen()) return "context"
938
- if (reviewTab() && hasReview()) return "review"
999
+ if (reviewTab() && hasAnyReview()) return "review"
939
1000
  return "empty"
940
1001
  })
941
1002
 
942
1003
  createEffect(() => {
943
1004
  if (!layout.ready()) return
944
1005
  if (tabs().active()) return
945
- if (openedTabs().length === 0 && !contextOpen() && !(reviewTab() && hasReview())) return
1006
+ if (openedTabs().length === 0 && !contextOpen() && !(reviewTab() && hasAnyReview())) return
946
1007
 
947
1008
  const next = activeTab()
948
1009
  if (next === "empty") return
@@ -958,7 +1019,7 @@ export default function Page() {
958
1019
 
959
1020
  if (opened) {
960
1021
  const active = tabs().active()
961
- const tab = active === "review" || (!active && hasReview()) ? "changes" : "all"
1022
+ const tab = active === "review" || (!active && hasAnyReview()) ? "changes" : "all"
962
1023
  layout.fileTree.setTab(tab)
963
1024
  }
964
1025
  },
@@ -974,9 +1035,19 @@ export default function Page() {
974
1035
  ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
975
1036
  : store.mobileTab === "changes"
976
1037
  if (!wants) return
977
- if (sync.data.session_diff[id] !== undefined) return
1038
+ if (store.changes === "project") {
1039
+ if (sync.data.project_diff[sdk.directory] !== undefined) return
1040
+ } else if (store.changes === "session") {
1041
+ if (sync.data.session_diff[id] !== undefined) return
1042
+ } else {
1043
+ return
1044
+ }
978
1045
  if (sync.status === "loading") return
979
1046
 
1047
+ if (store.changes === "project") {
1048
+ void sync.projectDiff.diff()
1049
+ return
1050
+ }
980
1051
  void sync.session.diff(id)
981
1052
  })
982
1053
 
@@ -1165,7 +1236,7 @@ export default function Page() {
1165
1236
  <SessionMobileTabs
1166
1237
  open={!isDesktop() && !!params.id}
1167
1238
  mobileTab={store.mobileTab}
1168
- hasReview={hasReview()}
1239
+ hasReview={hasAnyReview()}
1169
1240
  reviewCount={reviewCount()}
1170
1241
  onSession={() => setStore("mobileTab", "session")}
1171
1242
  onChanges={() => setStore("mobileTab", "changes")}
@@ -1190,6 +1261,8 @@ export default function Page() {
1190
1261
  mobileChanges={mobileChanges()}
1191
1262
  mobileFallback={reviewContent({
1192
1263
  diffStyle: "unified",
1264
+ wordWrap: layout.review.wordWrap(),
1265
+ onWordWrapChange: layout.review.setWordWrap,
1193
1266
  classes: {
1194
1267
  root: "pb-8",
1195
1268
  header: "px-4",
@@ -1281,7 +1354,17 @@ export default function Page() {
1281
1354
  </Show>
1282
1355
  </div>
1283
1356
 
1284
- <SessionSidePanel reviewPanel={reviewPanel} activeDiff={tree.activeDiff} focusReviewDiff={focusReviewDiff} />
1357
+ <SessionSidePanel
1358
+ reviewPanel={reviewPanel}
1359
+ activeDiff={tree.activeDiff}
1360
+ focusReviewDiff={focusReviewDiff}
1361
+ showReview={hasAnyReview}
1362
+ reviewCount={reviewCount}
1363
+ hasReview={hasReview}
1364
+ diffsReady={diffsReady}
1365
+ diffFiles={reviewDiffFiles}
1366
+ kinds={reviewKinds}
1367
+ />
1285
1368
  </div>
1286
1369
 
1287
1370
  <TerminalPanel />