ikanban-web 0.2.6 → 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/README.md +25 -20
- package/dist/assets/{ghostty-web-BsS8qmby.js → ghostty-web--axxBQj6.js} +1 -1
- package/dist/assets/{home-BVcfU4oW.js → home-BAEIhduF.js} +1 -1
- package/dist/assets/{index-B1DFsnCu.css → index-B0TEjAod.css} +1 -1
- package/dist/assets/{index-BbYqEnGb.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/session/session-header.tsx +2 -211
- 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/server.tsx +24 -4
- package/src/context/sync.tsx +237 -2
- package/src/i18n/en.ts +12 -1
- package/src/i18n/parity.test.ts +4 -22
- package/src/i18n/zh.ts +23 -1
- package/src/pages/layout.tsx +13 -56
- 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 -154
- package/src/pages/session.tsx +87 -16
- package/dist/assets/session-CZ-wC_3g.js +0 -24
- package/src/i18n/ar.ts +0 -751
- package/src/i18n/br.ts +0 -759
- package/src/i18n/bs.ts +0 -836
- package/src/i18n/da.ts +0 -830
- package/src/i18n/de.ts +0 -768
- package/src/i18n/es.ts +0 -842
- package/src/i18n/fr.ts +0 -766
- package/src/i18n/ja.ts +0 -755
- package/src/i18n/ko.ts +0 -755
- package/src/i18n/no.ts +0 -838
- package/src/i18n/pl.ts +0 -757
- package/src/i18n/ru.ts +0 -838
- package/src/i18n/th.ts +0 -828
- package/src/i18n/tr.ts +0 -849
- package/src/i18n/zht.ts +0 -821
package/src/i18n/zh.ts
CHANGED
|
@@ -28,6 +28,7 @@ export const dict = {
|
|
|
28
28
|
"command.sidebar.toggle": "切换侧边栏",
|
|
29
29
|
|
|
30
30
|
"command.project.open": "打开项目",
|
|
31
|
+
"command.project.close": "关闭项目",
|
|
31
32
|
|
|
32
33
|
"command.provider.connect": "连接提供商",
|
|
33
34
|
|
|
@@ -104,6 +105,8 @@ export const dict = {
|
|
|
104
105
|
|
|
105
106
|
"command.session.undo": "撤销",
|
|
106
107
|
"command.session.undo.description": "撤销上一条消息",
|
|
108
|
+
"command.session.timeline": "时间线",
|
|
109
|
+
"command.session.timeline.description": "跳转到此会话时间线中的任意消息",
|
|
107
110
|
"command.session.redo": "重做",
|
|
108
111
|
"command.session.redo.description": "重做上一条撤销的消息",
|
|
109
112
|
"command.session.compact": "精简会话",
|
|
@@ -139,7 +142,9 @@ export const dict = {
|
|
|
139
142
|
"dialog.model.search.placeholder": "搜索模型",
|
|
140
143
|
"dialog.model.empty": "未找到模型",
|
|
141
144
|
"dialog.model.manage": "管理模型",
|
|
145
|
+
"dialog.model.recent": "最近",
|
|
142
146
|
"dialog.model.manage.description": "自定义模型选择器中显示的模型。",
|
|
147
|
+
"dialog.model.manage.provider.toggle": "切换 {{provider}} 的所有模型",
|
|
143
148
|
"dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型",
|
|
144
149
|
"dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型",
|
|
145
150
|
|
|
@@ -320,6 +325,8 @@ export const dict = {
|
|
|
320
325
|
"mcp.status.disabled": "已禁用",
|
|
321
326
|
|
|
322
327
|
"dialog.fork.empty": "没有可用于分叉的消息",
|
|
328
|
+
"dialog.timeline.empty": "此时间线中没有消息",
|
|
329
|
+
"dialog.timeline.latest": "最新消息",
|
|
323
330
|
|
|
324
331
|
"dialog.directory.search.placeholder": "搜索文件夹",
|
|
325
332
|
"dialog.directory.empty": "未找到文件夹",
|
|
@@ -334,6 +341,11 @@ export const dict = {
|
|
|
334
341
|
"dialog.server.add.error": "无法连接到服务器",
|
|
335
342
|
"dialog.server.add.checking": "检查中...",
|
|
336
343
|
"dialog.server.add.button": "添加服务器",
|
|
344
|
+
"dialog.server.add.name": "服务器名称(可选)",
|
|
345
|
+
"dialog.server.add.namePlaceholder": "Localhost",
|
|
346
|
+
"dialog.server.add.username": "用户名(可选)",
|
|
347
|
+
"dialog.server.add.password": "密码(可选)",
|
|
348
|
+
"dialog.server.edit.title": "编辑服务器",
|
|
337
349
|
"dialog.server.default.title": "默认服务器",
|
|
338
350
|
"dialog.server.default.description": "应用启动时连接此服务器,而不是启动本地服务器。需要重启。",
|
|
339
351
|
"dialog.server.default.none": "未选择服务器",
|
|
@@ -406,6 +418,7 @@ export const dict = {
|
|
|
406
418
|
"language.br": "Português (Brasil)",
|
|
407
419
|
"language.bs": "Bosanski",
|
|
408
420
|
"language.th": "ไทย",
|
|
421
|
+
"language.tr": "Türkçe",
|
|
409
422
|
|
|
410
423
|
"toast.language.title": "语言",
|
|
411
424
|
"toast.language.description": "已切换到{{language}}",
|
|
@@ -497,6 +510,9 @@ export const dict = {
|
|
|
497
510
|
"session.review.change.other": "更改",
|
|
498
511
|
"session.review.loadingChanges": "正在加载更改...",
|
|
499
512
|
"session.review.empty": "此会话暂无更改",
|
|
513
|
+
"session.review.noVcs": "未检测到 Git 版本控制系统,因此无法检测会话更改",
|
|
514
|
+
"session.review.emptyProject": "当前没有项目更改",
|
|
515
|
+
"session.review.noVcsProject": "未检测到 Git 版本控制系统,因此无法检测项目更改",
|
|
500
516
|
"session.review.noChanges": "无更改",
|
|
501
517
|
"session.files.selectToOpen": "选择要打开的文件",
|
|
502
518
|
"session.files.all": "所有文件",
|
|
@@ -581,7 +597,7 @@ export const dict = {
|
|
|
581
597
|
"sidebar.project.viewAllSessions": "查看全部会话",
|
|
582
598
|
"sidebar.project.clearNotifications": "清除通知",
|
|
583
599
|
|
|
584
|
-
"app.name.desktop": "
|
|
600
|
+
"app.name.desktop": "iKanban",
|
|
585
601
|
|
|
586
602
|
"settings.section.desktop": "桌面",
|
|
587
603
|
"settings.section.server": "服务器",
|
|
@@ -597,6 +613,7 @@ export const dict = {
|
|
|
597
613
|
"settings.general.section.notifications": "系统通知",
|
|
598
614
|
"settings.general.section.updates": "更新",
|
|
599
615
|
"settings.general.section.sounds": "音效",
|
|
616
|
+
"settings.general.section.releaseNotes": "发行说明",
|
|
600
617
|
"settings.general.section.feed": "动态",
|
|
601
618
|
"settings.general.section.display": "显示",
|
|
602
619
|
"settings.general.row.language.title": "语言",
|
|
@@ -607,6 +624,8 @@ export const dict = {
|
|
|
607
624
|
"settings.general.row.theme.description": "自定义 OpenCode 的主题。",
|
|
608
625
|
"settings.general.row.font.title": "字体",
|
|
609
626
|
"settings.general.row.font.description": "自定义代码块使用的等宽字体",
|
|
627
|
+
"settings.general.row.reasoningSummaries.title": "显示推理摘要",
|
|
628
|
+
"settings.general.row.reasoningSummaries.description": "在时间线中显示模型推理摘要",
|
|
610
629
|
"settings.general.row.shellToolPartsExpanded.title": "展开 shell 工具部分",
|
|
611
630
|
"settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 shell 工具部分",
|
|
612
631
|
"settings.general.row.editToolPartsExpanded.title": "展开编辑工具部分",
|
|
@@ -616,6 +635,9 @@ export const dict = {
|
|
|
616
635
|
"settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。",
|
|
617
636
|
"settings.general.row.releaseNotes.title": "发行说明",
|
|
618
637
|
"settings.general.row.releaseNotes.description": "更新后显示“新功能”弹窗",
|
|
638
|
+
"settings.general.row.releaseNotesCurrent.title": "当前版本变更",
|
|
639
|
+
"settings.general.row.releaseNotesCurrent.description": "在 GitHub 打开当前版本的更新记录",
|
|
640
|
+
"settings.general.action.releaseNotesCurrent": "查看变更",
|
|
619
641
|
|
|
620
642
|
"settings.updates.row.startup.title": "启动时检查更新",
|
|
621
643
|
"settings.updates.row.startup.description": "在 OpenCode 启动时自动检查更新",
|
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"),
|
|
@@ -2080,7 +2037,7 @@ export default function Layout(props: ParentProps) {
|
|
|
2080
2037
|
settingsKeybind={() => command.keybind("settings.open")}
|
|
2081
2038
|
onOpenSettings={openSettings}
|
|
2082
2039
|
helpLabel={() => language.t("sidebar.help")}
|
|
2083
|
-
onOpenHelp={() => platform.openLink("https://
|
|
2040
|
+
onOpenHelp={() => platform.openLink("https://github.com/isomoes/ikanban/issues")}
|
|
2084
2041
|
renderPanel={() => (
|
|
2085
2042
|
<Show when={currentProject()} keyed>
|
|
2086
2043
|
{(project) => <SidebarPanel project={project} />}
|
|
@@ -2149,7 +2106,7 @@ export default function Layout(props: ParentProps) {
|
|
|
2149
2106
|
settingsKeybind={() => command.keybind("settings.open")}
|
|
2150
2107
|
onOpenSettings={openSettings}
|
|
2151
2108
|
helpLabel={() => language.t("sidebar.help")}
|
|
2152
|
-
onOpenHelp={() => platform.openLink("https://
|
|
2109
|
+
onOpenHelp={() => platform.openLink("https://github.com/isomoes/ikanban/issues")}
|
|
2153
2110
|
renderPanel={() => <SidebarPanel project={currentProject()} mobile />}
|
|
2154
2111
|
/>
|
|
2155
2112
|
</nav>
|
|
@@ -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({
|
|
@@ -404,126 +462,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|
|
404
462
|
}),
|
|
405
463
|
]);
|
|
406
464
|
|
|
407
|
-
const shareCommands = createMemo(() => {
|
|
408
|
-
if (sync.data.config.share === "disabled") return [];
|
|
409
|
-
return [
|
|
410
|
-
sessionCommand({
|
|
411
|
-
id: "session.share",
|
|
412
|
-
title: info()?.share?.url
|
|
413
|
-
? language.t("session.share.copy.copyLink")
|
|
414
|
-
: language.t("command.session.share"),
|
|
415
|
-
description: info()?.share?.url
|
|
416
|
-
? language.t("toast.session.share.success.description")
|
|
417
|
-
: language.t("command.session.share.description"),
|
|
418
|
-
slash: "share",
|
|
419
|
-
disabled: !params.id,
|
|
420
|
-
onSelect: async () => {
|
|
421
|
-
if (!params.id) return;
|
|
422
|
-
|
|
423
|
-
const write = (value: string) => {
|
|
424
|
-
const body =
|
|
425
|
-
typeof document === "undefined" ? undefined : document.body;
|
|
426
|
-
if (body) {
|
|
427
|
-
const textarea = document.createElement("textarea");
|
|
428
|
-
textarea.value = value;
|
|
429
|
-
textarea.setAttribute("readonly", "");
|
|
430
|
-
textarea.style.position = "fixed";
|
|
431
|
-
textarea.style.opacity = "0";
|
|
432
|
-
textarea.style.pointerEvents = "none";
|
|
433
|
-
body.appendChild(textarea);
|
|
434
|
-
textarea.select();
|
|
435
|
-
const copied = document.execCommand("copy");
|
|
436
|
-
body.removeChild(textarea);
|
|
437
|
-
if (copied) return Promise.resolve(true);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const clipboard =
|
|
441
|
-
typeof navigator === "undefined"
|
|
442
|
-
? undefined
|
|
443
|
-
: navigator.clipboard;
|
|
444
|
-
if (!clipboard?.writeText) return Promise.resolve(false);
|
|
445
|
-
return clipboard.writeText(value).then(
|
|
446
|
-
() => true,
|
|
447
|
-
() => false,
|
|
448
|
-
);
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
const copy = async (url: string, existing: boolean) => {
|
|
452
|
-
const ok = await write(url);
|
|
453
|
-
if (!ok) {
|
|
454
|
-
showToast({
|
|
455
|
-
title: language.t("toast.session.share.copyFailed.title"),
|
|
456
|
-
variant: "error",
|
|
457
|
-
});
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
showToast({
|
|
462
|
-
title: existing
|
|
463
|
-
? language.t("session.share.copy.copied")
|
|
464
|
-
: language.t("toast.session.share.success.title"),
|
|
465
|
-
description: language.t(
|
|
466
|
-
"toast.session.share.success.description",
|
|
467
|
-
),
|
|
468
|
-
variant: "success",
|
|
469
|
-
});
|
|
470
|
-
};
|
|
471
|
-
|
|
472
|
-
const existing = info()?.share?.url;
|
|
473
|
-
if (existing) {
|
|
474
|
-
await copy(existing, true);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const url = await sdk.client.session
|
|
479
|
-
.share({ sessionID: params.id })
|
|
480
|
-
.then((res) => res.data?.share?.url)
|
|
481
|
-
.catch(() => undefined);
|
|
482
|
-
if (!url) {
|
|
483
|
-
showToast({
|
|
484
|
-
title: language.t("toast.session.share.failed.title"),
|
|
485
|
-
description: language.t("toast.session.share.failed.description"),
|
|
486
|
-
variant: "error",
|
|
487
|
-
});
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
await copy(url, false);
|
|
492
|
-
},
|
|
493
|
-
}),
|
|
494
|
-
sessionCommand({
|
|
495
|
-
id: "session.unshare",
|
|
496
|
-
title: language.t("command.session.unshare"),
|
|
497
|
-
description: language.t("command.session.unshare.description"),
|
|
498
|
-
slash: "unshare",
|
|
499
|
-
disabled: !params.id || !info()?.share?.url,
|
|
500
|
-
onSelect: async () => {
|
|
501
|
-
if (!params.id) return;
|
|
502
|
-
await sdk.client.session
|
|
503
|
-
.unshare({ sessionID: params.id })
|
|
504
|
-
.then(() =>
|
|
505
|
-
showToast({
|
|
506
|
-
title: language.t("toast.session.unshare.success.title"),
|
|
507
|
-
description: language.t(
|
|
508
|
-
"toast.session.unshare.success.description",
|
|
509
|
-
),
|
|
510
|
-
variant: "success",
|
|
511
|
-
}),
|
|
512
|
-
)
|
|
513
|
-
.catch(() =>
|
|
514
|
-
showToast({
|
|
515
|
-
title: language.t("toast.session.unshare.failed.title"),
|
|
516
|
-
description: language.t(
|
|
517
|
-
"toast.session.unshare.failed.description",
|
|
518
|
-
),
|
|
519
|
-
variant: "error",
|
|
520
|
-
}),
|
|
521
|
-
);
|
|
522
|
-
},
|
|
523
|
-
}),
|
|
524
|
-
];
|
|
525
|
-
});
|
|
526
|
-
|
|
527
465
|
command.register("session", () =>
|
|
528
466
|
[
|
|
529
467
|
sessionCommands(),
|
|
@@ -534,7 +472,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
|
|
534
472
|
agentCommands(),
|
|
535
473
|
permissionCommands(),
|
|
536
474
|
sessionActionCommands(),
|
|
537
|
-
shareCommands(),
|
|
538
475
|
].flatMap((x) => x),
|
|
539
476
|
);
|
|
540
477
|
};
|