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.
- package/dist/assets/{ghostty-web-BnnMBzi5.js → ghostty-web--axxBQj6.js} +1 -1
- package/dist/assets/{home-DeLBKxk3.js → home-BAEIhduF.js} +1 -1
- package/dist/assets/{index-pnX-hTQP.css → index-B0TEjAod.css} +1 -1
- package/dist/assets/{index-ehO8js0v.js → index-BR3vRMWX.js} +124 -116
- package/dist/assets/session-Bp9iU6SU.js +24 -0
- package/dist/index.html +19 -6
- package/package.json +1 -1
- package/src/components/dialog-session-timeline.tsx +85 -0
- package/src/components/prompt-input.tsx +3 -3
- package/src/components/settings-general.tsx +18 -93
- package/src/context/global-sync/child-store.ts +1 -0
- package/src/context/global-sync/event-reducer.test.ts +5 -0
- package/src/context/global-sync/event-reducer.ts +263 -183
- package/src/context/global-sync/types.ts +3 -0
- package/src/context/highlights.tsx +4 -176
- package/src/context/sync.tsx +237 -2
- package/src/i18n/en.ts +11 -0
- package/src/i18n/zh.ts +22 -0
- package/src/pages/layout.tsx +11 -54
- package/src/pages/session/review-tab.tsx +3 -1
- package/src/pages/session/session-side-panel.tsx +17 -51
- package/src/pages/session/use-session-commands.tsx +91 -33
- package/src/pages/session.tsx +87 -16
- package/dist/assets/session-D7vMISoz.js +0 -24
package/src/pages/layout.tsx
CHANGED
|
@@ -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() &&
|
|
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
|
|
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
|
-
|
|
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({
|
package/src/pages/session.tsx
CHANGED
|
@@ -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
|
|
364
|
-
const
|
|
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: "
|
|
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(() =>
|
|
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", "
|
|
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 === "
|
|
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() &&
|
|
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() &&
|
|
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 &&
|
|
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 (
|
|
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={
|
|
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
|
|
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 />
|