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.
- package/dist/assets/{ghostty-web-BnnMBzi5.js → ghostty-web-CveYg7XI.js} +1 -1
- package/dist/assets/{home-DeLBKxk3.js → home-OHGVA1hY.js} +1 -1
- package/dist/assets/{index-pnX-hTQP.css → index-B0TEjAod.css} +1 -1
- package/dist/assets/{index-ehO8js0v.js → index-BlcL7wgr.js} +132 -124
- package/dist/assets/session-CdIG8nTN.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/layout.tsx +12 -2
- 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 +7 -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 +99 -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"),
|
|
@@ -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() &&
|
|
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"
|
|
@@ -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() &&
|
|
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() &&
|
|
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 &&
|
|
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 (
|
|
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={
|
|
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
|
|
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 />
|