ikanban-web 0.2.7 → 0.2.8

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"),
@@ -21,6 +21,7 @@ export interface SessionReviewTabProps {
21
21
  diffStyle: DiffStyle
22
22
  onDiffStyleChange?: (style: DiffStyle) => void
23
23
  onViewFile?: (file: string) => void
24
+ onRevealFile?: (file: string) => void
24
25
  onLineComment?: (comment: { file: string; selection: SelectedLineRange; comment: string; preview?: string }) => void
25
26
  onLineCommentUpdate?: (comment: SessionReviewCommentUpdate) => void
26
27
  onLineCommentDelete?: (comment: SessionReviewCommentDelete) => void
@@ -56,7 +57,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
56
57
 
57
58
  const readFile = async (path: string) => {
58
59
  return sdk.client.file
59
- .read({ path })
60
+ .read({ path, directory: sdk.directory })
60
61
  .then((x) => x.data)
61
62
  .catch((error) => {
62
63
  console.debug("[session-review] failed to read file", { path, error })
@@ -164,6 +165,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
164
165
  diffStyle={props.diffStyle}
165
166
  onDiffStyleChange={props.onDiffStyleChange}
166
167
  onViewFile={props.onViewFile}
168
+ onRevealFile={props.onRevealFile}
167
169
  focusedFile={props.focusedFile}
168
170
  readFile={readFile}
169
171
  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"
@@ -743,6 +786,10 @@ export default function Page() {
743
786
  title={changesTitle()}
744
787
  empty={emptyTurn()}
745
788
  diffs={reviewDiffs}
789
+ onRevealFile={(file) => {
790
+ if (store.changes !== "project") return
791
+ void sync.projectDiff.hydrate(file)
792
+ }}
746
793
  view={view}
747
794
  diffStyle={input.diffStyle}
748
795
  onDiffStyleChange={input.onDiffStyleChange}
@@ -767,6 +814,10 @@ export default function Page() {
767
814
  <SessionReviewTab
768
815
  title={changesTitle()}
769
816
  diffs={reviewDiffs}
817
+ onRevealFile={(file) => {
818
+ if (store.changes !== "project") return
819
+ void sync.projectDiff.hydrate(file)
820
+ }}
770
821
  view={view}
771
822
  diffStyle={input.diffStyle}
772
823
  onDiffStyleChange={input.onDiffStyleChange}
@@ -935,14 +986,14 @@ export default function Page() {
935
986
  const first = openedTabs()[0]
936
987
  if (first) return first
937
988
  if (contextOpen()) return "context"
938
- if (reviewTab() && hasReview()) return "review"
989
+ if (reviewTab() && hasAnyReview()) return "review"
939
990
  return "empty"
940
991
  })
941
992
 
942
993
  createEffect(() => {
943
994
  if (!layout.ready()) return
944
995
  if (tabs().active()) return
945
- if (openedTabs().length === 0 && !contextOpen() && !(reviewTab() && hasReview())) return
996
+ if (openedTabs().length === 0 && !contextOpen() && !(reviewTab() && hasAnyReview())) return
946
997
 
947
998
  const next = activeTab()
948
999
  if (next === "empty") return
@@ -958,7 +1009,7 @@ export default function Page() {
958
1009
 
959
1010
  if (opened) {
960
1011
  const active = tabs().active()
961
- const tab = active === "review" || (!active && hasReview()) ? "changes" : "all"
1012
+ const tab = active === "review" || (!active && hasAnyReview()) ? "changes" : "all"
962
1013
  layout.fileTree.setTab(tab)
963
1014
  }
964
1015
  },
@@ -974,9 +1025,19 @@ export default function Page() {
974
1025
  ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
975
1026
  : store.mobileTab === "changes"
976
1027
  if (!wants) return
977
- if (sync.data.session_diff[id] !== undefined) return
1028
+ if (store.changes === "project") {
1029
+ if (sync.data.project_diff[sdk.directory] !== undefined) return
1030
+ } else if (store.changes === "session") {
1031
+ if (sync.data.session_diff[id] !== undefined) return
1032
+ } else {
1033
+ return
1034
+ }
978
1035
  if (sync.status === "loading") return
979
1036
 
1037
+ if (store.changes === "project") {
1038
+ void sync.projectDiff.diff()
1039
+ return
1040
+ }
980
1041
  void sync.session.diff(id)
981
1042
  })
982
1043
 
@@ -1165,7 +1226,7 @@ export default function Page() {
1165
1226
  <SessionMobileTabs
1166
1227
  open={!isDesktop() && !!params.id}
1167
1228
  mobileTab={store.mobileTab}
1168
- hasReview={hasReview()}
1229
+ hasReview={hasAnyReview()}
1169
1230
  reviewCount={reviewCount()}
1170
1231
  onSession={() => setStore("mobileTab", "session")}
1171
1232
  onChanges={() => setStore("mobileTab", "changes")}
@@ -1281,7 +1342,17 @@ export default function Page() {
1281
1342
  </Show>
1282
1343
  </div>
1283
1344
 
1284
- <SessionSidePanel reviewPanel={reviewPanel} activeDiff={tree.activeDiff} focusReviewDiff={focusReviewDiff} />
1345
+ <SessionSidePanel
1346
+ reviewPanel={reviewPanel}
1347
+ activeDiff={tree.activeDiff}
1348
+ focusReviewDiff={focusReviewDiff}
1349
+ showReview={hasAnyReview}
1350
+ reviewCount={reviewCount}
1351
+ hasReview={hasReview}
1352
+ diffsReady={diffsReady}
1353
+ diffFiles={reviewDiffFiles}
1354
+ kinds={reviewKinds}
1355
+ />
1285
1356
  </div>
1286
1357
 
1287
1358
  <TerminalPanel />