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/dist/index.html
CHANGED
|
@@ -3,19 +3,32 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title>
|
|
7
|
-
<link
|
|
6
|
+
<title>IKanban</title>
|
|
7
|
+
<link
|
|
8
|
+
rel="icon"
|
|
9
|
+
type="image/png"
|
|
10
|
+
href="/ikanban/favicon-96x96-v3.png"
|
|
11
|
+
sizes="96x96"
|
|
12
|
+
/>
|
|
8
13
|
<link rel="icon" type="image/svg+xml" href="/ikanban/favicon-v3.svg" />
|
|
9
14
|
<link rel="shortcut icon" href="/ikanban/favicon-v3.ico" />
|
|
10
|
-
<link
|
|
15
|
+
<link
|
|
16
|
+
rel="apple-touch-icon"
|
|
17
|
+
sizes="180x180"
|
|
18
|
+
href="/ikanban/apple-touch-icon-v3.png"
|
|
19
|
+
/>
|
|
11
20
|
<link rel="manifest" href="/ikanban/site.webmanifest" />
|
|
12
21
|
<meta name="theme-color" content="#F8F7F7" />
|
|
13
|
-
<meta
|
|
22
|
+
<meta
|
|
23
|
+
name="theme-color"
|
|
24
|
+
content="#131010"
|
|
25
|
+
media="(prefers-color-scheme: dark)"
|
|
26
|
+
/>
|
|
14
27
|
<meta property="og:image" content="/ikanban/social-share.png" />
|
|
15
28
|
<meta property="twitter:image" content="/social-share.png" />
|
|
16
29
|
<script id="oc-theme-preload-script" src="/ikanban/oc-theme-preload.js"></script>
|
|
17
|
-
<script type="module" crossorigin src="/ikanban/assets/index-
|
|
18
|
-
<link rel="stylesheet" crossorigin href="/ikanban/assets/index-
|
|
30
|
+
<script type="module" crossorigin src="/ikanban/assets/index-BR3vRMWX.js"></script>
|
|
31
|
+
<link rel="stylesheet" crossorigin href="/ikanban/assets/index-B0TEjAod.css">
|
|
19
32
|
</head>
|
|
20
33
|
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
|
21
34
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
package/package.json
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Component, createMemo } from "solid-js"
|
|
2
|
+
import { useParams } from "@solidjs/router"
|
|
3
|
+
import { useSync } from "@/context/sync"
|
|
4
|
+
import { useLanguage } from "@/context/language"
|
|
5
|
+
import { Dialog } from "ikanban-ui/dialog"
|
|
6
|
+
import { List } from "ikanban-ui/list"
|
|
7
|
+
import type { TextPart as SDKTextPart, UserMessage } from "@opencode-ai/sdk/v2/client"
|
|
8
|
+
|
|
9
|
+
interface TimelineMessage {
|
|
10
|
+
id: string
|
|
11
|
+
text: string
|
|
12
|
+
time: string
|
|
13
|
+
message?: UserMessage
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatTime(date: Date): string {
|
|
17
|
+
return date.toLocaleTimeString(undefined, { timeStyle: "short" })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DialogSessionTimeline: Component<{
|
|
21
|
+
onSelect: (message: UserMessage | undefined) => void
|
|
22
|
+
}> = (props) => {
|
|
23
|
+
const params = useParams()
|
|
24
|
+
const sync = useSync()
|
|
25
|
+
const language = useLanguage()
|
|
26
|
+
|
|
27
|
+
const messages = createMemo((): TimelineMessage[] => {
|
|
28
|
+
const sessionID = params.id
|
|
29
|
+
if (!sessionID) return []
|
|
30
|
+
|
|
31
|
+
const revert = sync.session.get(sessionID)?.revert?.messageID
|
|
32
|
+
const sessionMessages = sync.data.message[sessionID] ?? []
|
|
33
|
+
const result: TimelineMessage[] = []
|
|
34
|
+
let latestUserMessage: UserMessage | undefined
|
|
35
|
+
|
|
36
|
+
for (const message of sessionMessages) {
|
|
37
|
+
if (message.role !== "user") continue
|
|
38
|
+
latestUserMessage = message
|
|
39
|
+
|
|
40
|
+
const parts = sync.data.part[message.id] ?? []
|
|
41
|
+
const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored)
|
|
42
|
+
if (!textPart) continue
|
|
43
|
+
|
|
44
|
+
result.push({
|
|
45
|
+
id: message.id,
|
|
46
|
+
text: textPart.text.replace(/\n/g, " ").slice(0, 200),
|
|
47
|
+
time: formatTime(new Date(message.time.created)),
|
|
48
|
+
message,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
result.reverse()
|
|
53
|
+
|
|
54
|
+
if (revert && latestUserMessage) {
|
|
55
|
+
result.unshift({
|
|
56
|
+
id: "latest",
|
|
57
|
+
text: language.t("dialog.timeline.latest"),
|
|
58
|
+
time: formatTime(new Date(latestUserMessage.time.created)),
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Dialog title={language.t("command.session.timeline")}>
|
|
67
|
+
<List
|
|
68
|
+
class="flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0"
|
|
69
|
+
search={{ placeholder: language.t("common.search.placeholder"), autofocus: true }}
|
|
70
|
+
emptyMessage={language.t("dialog.timeline.empty")}
|
|
71
|
+
key={(item) => item.id}
|
|
72
|
+
items={messages}
|
|
73
|
+
filterKeys={["text"]}
|
|
74
|
+
onSelect={(item) => props.onSelect(item?.message)}
|
|
75
|
+
>
|
|
76
|
+
{(item) => (
|
|
77
|
+
<div class="w-full flex items-center gap-2">
|
|
78
|
+
<span class="truncate flex-1 min-w-0 text-left font-normal">{item.text}</span>
|
|
79
|
+
<span class="text-text-weak shrink-0 font-normal">{item.time}</span>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</List>
|
|
83
|
+
</Dialog>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -162,9 +162,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
162
162
|
const sessionID = params.id
|
|
163
163
|
if (!sessionID) return false
|
|
164
164
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
return
|
|
165
|
+
const sessionDiffs = sync.data.session_diff[sessionID] ?? []
|
|
166
|
+
const projectDiffs = sync.data.project_diff[sync.directory] ?? []
|
|
167
|
+
return [...sessionDiffs, ...projectDiffs].some((diff) => diff.file === path)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
const openComment = (item: { path: string; commentID?: string; commentOrigin?: "review" | "file" }) => {
|
|
@@ -4,23 +4,19 @@ import { DropdownMenu } from "ikanban-ui/dropdown-menu"
|
|
|
4
4
|
import { Icon } from "ikanban-ui/icon"
|
|
5
5
|
import { IconButton } from "ikanban-ui/icon-button"
|
|
6
6
|
import { Keybind } from "ikanban-ui/keybind"
|
|
7
|
-
import { Popover } from "ikanban-ui/popover"
|
|
8
7
|
import { Spinner } from "ikanban-ui/spinner"
|
|
9
|
-
import { TextField } from "ikanban-ui/text-field"
|
|
10
8
|
import { showToast } from "ikanban-ui/toast"
|
|
11
|
-
import {
|
|
9
|
+
import { TooltipKeybind } from "ikanban-ui/tooltip"
|
|
12
10
|
import { getFilename } from "ikanban-utils/path"
|
|
13
11
|
import { useParams } from "@solidjs/router"
|
|
14
|
-
import { createEffect, createMemo, For,
|
|
12
|
+
import { createEffect, createMemo, For, Show } from "solid-js"
|
|
15
13
|
import { createStore } from "solid-js/store"
|
|
16
14
|
import { Portal } from "solid-js/web"
|
|
17
15
|
import { useCommand } from "@/context/command"
|
|
18
|
-
import { useGlobalSDK } from "@/context/global-sdk"
|
|
19
16
|
import { useLanguage } from "@/context/language"
|
|
20
17
|
import { useLayout } from "@/context/layout"
|
|
21
18
|
import { usePlatform } from "@/context/platform"
|
|
22
19
|
import { useServer } from "@/context/server"
|
|
23
|
-
import { useSync } from "@/context/sync"
|
|
24
20
|
import { decode64 } from "@/utils/base64"
|
|
25
21
|
import { Persist, persisted } from "@/utils/persist"
|
|
26
22
|
import { StatusPopover } from "../status-popover"
|
|
@@ -134,99 +130,11 @@ const showRequestError = (language: ReturnType<typeof useLanguage>, err: unknown
|
|
|
134
130
|
})
|
|
135
131
|
}
|
|
136
132
|
|
|
137
|
-
function useSessionShare(args: {
|
|
138
|
-
globalSDK: ReturnType<typeof useGlobalSDK>
|
|
139
|
-
currentSession: () =>
|
|
140
|
-
| {
|
|
141
|
-
share?: {
|
|
142
|
-
url?: string
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
| undefined
|
|
146
|
-
sessionID: () => string | undefined
|
|
147
|
-
projectDirectory: () => string
|
|
148
|
-
platform: ReturnType<typeof usePlatform>
|
|
149
|
-
}) {
|
|
150
|
-
const [state, setState] = createStore({
|
|
151
|
-
share: false,
|
|
152
|
-
unshare: false,
|
|
153
|
-
copied: false,
|
|
154
|
-
timer: undefined as number | undefined,
|
|
155
|
-
})
|
|
156
|
-
const shareUrl = createMemo(() => args.currentSession()?.share?.url)
|
|
157
|
-
|
|
158
|
-
createEffect(() => {
|
|
159
|
-
const url = shareUrl()
|
|
160
|
-
if (url) return
|
|
161
|
-
if (state.timer) window.clearTimeout(state.timer)
|
|
162
|
-
setState({ copied: false, timer: undefined })
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
onCleanup(() => {
|
|
166
|
-
if (state.timer) window.clearTimeout(state.timer)
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
const shareSession = () => {
|
|
170
|
-
const sessionID = args.sessionID()
|
|
171
|
-
if (!sessionID || state.share) return
|
|
172
|
-
setState("share", true)
|
|
173
|
-
args.globalSDK.client.session
|
|
174
|
-
.share({ sessionID, directory: args.projectDirectory() })
|
|
175
|
-
.catch((error) => {
|
|
176
|
-
console.error("Failed to share session", error)
|
|
177
|
-
})
|
|
178
|
-
.finally(() => {
|
|
179
|
-
setState("share", false)
|
|
180
|
-
})
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const unshareSession = () => {
|
|
184
|
-
const sessionID = args.sessionID()
|
|
185
|
-
if (!sessionID || state.unshare) return
|
|
186
|
-
setState("unshare", true)
|
|
187
|
-
args.globalSDK.client.session
|
|
188
|
-
.unshare({ sessionID, directory: args.projectDirectory() })
|
|
189
|
-
.catch((error) => {
|
|
190
|
-
console.error("Failed to unshare session", error)
|
|
191
|
-
})
|
|
192
|
-
.finally(() => {
|
|
193
|
-
setState("unshare", false)
|
|
194
|
-
})
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const copyLink = (onError: (error: unknown) => void) => {
|
|
198
|
-
const url = shareUrl()
|
|
199
|
-
if (!url) return
|
|
200
|
-
navigator.clipboard
|
|
201
|
-
.writeText(url)
|
|
202
|
-
.then(() => {
|
|
203
|
-
if (state.timer) window.clearTimeout(state.timer)
|
|
204
|
-
setState("copied", true)
|
|
205
|
-
const timer = window.setTimeout(() => {
|
|
206
|
-
setState("copied", false)
|
|
207
|
-
setState("timer", undefined)
|
|
208
|
-
}, 3000)
|
|
209
|
-
setState("timer", timer)
|
|
210
|
-
})
|
|
211
|
-
.catch(onError)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const viewShare = () => {
|
|
215
|
-
const url = shareUrl()
|
|
216
|
-
if (!url) return
|
|
217
|
-
args.platform.openLink(url)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return { state, shareUrl, shareSession, unshareSession, copyLink, viewShare }
|
|
221
|
-
}
|
|
222
|
-
|
|
223
133
|
export function SessionHeader() {
|
|
224
|
-
const globalSDK = useGlobalSDK()
|
|
225
134
|
const layout = useLayout()
|
|
226
135
|
const params = useParams()
|
|
227
136
|
const command = useCommand()
|
|
228
137
|
const server = useServer()
|
|
229
|
-
const sync = useSync()
|
|
230
138
|
const platform = usePlatform()
|
|
231
139
|
const language = useLanguage()
|
|
232
140
|
|
|
@@ -243,9 +151,6 @@ export function SessionHeader() {
|
|
|
243
151
|
})
|
|
244
152
|
const hotkey = createMemo(() => command.keybind("file.open"))
|
|
245
153
|
|
|
246
|
-
const currentSession = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
|
247
|
-
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
|
|
248
|
-
const showShare = createMemo(() => shareEnabled() && !!params.id)
|
|
249
154
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
|
250
155
|
const view = createMemo(() => layout.view(sessionKey))
|
|
251
156
|
const os = createMemo(() => detectOS(platform))
|
|
@@ -343,14 +248,6 @@ export function SessionHeader() {
|
|
|
343
248
|
.catch((err: unknown) => showRequestError(language, err))
|
|
344
249
|
}
|
|
345
250
|
|
|
346
|
-
const share = useSessionShare({
|
|
347
|
-
globalSDK,
|
|
348
|
-
currentSession,
|
|
349
|
-
sessionID: () => params.id,
|
|
350
|
-
projectDirectory,
|
|
351
|
-
platform,
|
|
352
|
-
})
|
|
353
|
-
|
|
354
251
|
const centerMount = createMemo(() => document.getElementById("opencode-titlebar-center"))
|
|
355
252
|
const rightMount = createMemo(() => document.getElementById("opencode-titlebar-right"))
|
|
356
253
|
|
|
@@ -505,112 +402,6 @@ export function SessionHeader() {
|
|
|
505
402
|
</Show>
|
|
506
403
|
</div>
|
|
507
404
|
</Show>
|
|
508
|
-
<Show when={showShare()}>
|
|
509
|
-
<div class="flex items-center">
|
|
510
|
-
<Popover
|
|
511
|
-
title={language.t("session.share.popover.title")}
|
|
512
|
-
description={
|
|
513
|
-
share.shareUrl()
|
|
514
|
-
? language.t("session.share.popover.description.shared")
|
|
515
|
-
: language.t("session.share.popover.description.unshared")
|
|
516
|
-
}
|
|
517
|
-
gutter={4}
|
|
518
|
-
placement="bottom-end"
|
|
519
|
-
shift={-64}
|
|
520
|
-
class="rounded-xl [&_[data-slot=popover-close-button]]:hidden"
|
|
521
|
-
triggerAs={Button}
|
|
522
|
-
triggerProps={{
|
|
523
|
-
variant: "ghost",
|
|
524
|
-
class:
|
|
525
|
-
"rounded-md h-[24px] px-3 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-base-active",
|
|
526
|
-
classList: {
|
|
527
|
-
"rounded-r-none": share.shareUrl() !== undefined,
|
|
528
|
-
"border-r-0": share.shareUrl() !== undefined,
|
|
529
|
-
},
|
|
530
|
-
style: { scale: 1 },
|
|
531
|
-
}}
|
|
532
|
-
trigger={<span class="text-12-regular">{language.t("session.share.action.share")}</span>}
|
|
533
|
-
>
|
|
534
|
-
<div class="flex flex-col gap-2">
|
|
535
|
-
<Show
|
|
536
|
-
when={share.shareUrl()}
|
|
537
|
-
fallback={
|
|
538
|
-
<div class="flex">
|
|
539
|
-
<Button
|
|
540
|
-
size="large"
|
|
541
|
-
variant="primary"
|
|
542
|
-
class="w-1/2"
|
|
543
|
-
onClick={share.shareSession}
|
|
544
|
-
disabled={share.state.share}
|
|
545
|
-
>
|
|
546
|
-
{share.state.share
|
|
547
|
-
? language.t("session.share.action.publishing")
|
|
548
|
-
: language.t("session.share.action.publish")}
|
|
549
|
-
</Button>
|
|
550
|
-
</div>
|
|
551
|
-
}
|
|
552
|
-
>
|
|
553
|
-
<div class="flex flex-col gap-2">
|
|
554
|
-
<TextField
|
|
555
|
-
value={share.shareUrl() ?? ""}
|
|
556
|
-
readOnly
|
|
557
|
-
copyable
|
|
558
|
-
copyKind="link"
|
|
559
|
-
tabIndex={-1}
|
|
560
|
-
class="w-full"
|
|
561
|
-
/>
|
|
562
|
-
<div class="grid grid-cols-2 gap-2">
|
|
563
|
-
<Button
|
|
564
|
-
size="large"
|
|
565
|
-
variant="secondary"
|
|
566
|
-
class="w-full shadow-none border border-border-weak-base"
|
|
567
|
-
onClick={share.unshareSession}
|
|
568
|
-
disabled={share.state.unshare}
|
|
569
|
-
>
|
|
570
|
-
{share.state.unshare
|
|
571
|
-
? language.t("session.share.action.unpublishing")
|
|
572
|
-
: language.t("session.share.action.unpublish")}
|
|
573
|
-
</Button>
|
|
574
|
-
<Button
|
|
575
|
-
size="large"
|
|
576
|
-
variant="primary"
|
|
577
|
-
class="w-full"
|
|
578
|
-
onClick={share.viewShare}
|
|
579
|
-
disabled={share.state.unshare}
|
|
580
|
-
>
|
|
581
|
-
{language.t("session.share.action.view")}
|
|
582
|
-
</Button>
|
|
583
|
-
</div>
|
|
584
|
-
</div>
|
|
585
|
-
</Show>
|
|
586
|
-
</div>
|
|
587
|
-
</Popover>
|
|
588
|
-
<Show when={share.shareUrl()} fallback={<div aria-hidden="true" />}>
|
|
589
|
-
<Tooltip
|
|
590
|
-
value={
|
|
591
|
-
share.state.copied
|
|
592
|
-
? language.t("session.share.copy.copied")
|
|
593
|
-
: language.t("session.share.copy.copyLink")
|
|
594
|
-
}
|
|
595
|
-
placement="top"
|
|
596
|
-
gutter={8}
|
|
597
|
-
>
|
|
598
|
-
<IconButton
|
|
599
|
-
icon={share.state.copied ? "check" : "link"}
|
|
600
|
-
variant="ghost"
|
|
601
|
-
class="rounded-l-none h-[24px] border border-border-weak-base bg-surface-panel shadow-none"
|
|
602
|
-
onClick={() => share.copyLink((error) => showRequestError(language, error))}
|
|
603
|
-
disabled={share.state.unshare}
|
|
604
|
-
aria-label={
|
|
605
|
-
share.state.copied
|
|
606
|
-
? language.t("session.share.copy.copied")
|
|
607
|
-
: language.t("session.share.copy.copyLink")
|
|
608
|
-
}
|
|
609
|
-
/>
|
|
610
|
-
</Tooltip>
|
|
611
|
-
</Show>
|
|
612
|
-
</div>
|
|
613
|
-
</Show>
|
|
614
405
|
<div class="flex items-center gap-1">
|
|
615
406
|
<div class="hidden md:flex items-center gap-1 shrink-0">
|
|
616
407
|
<TooltipKeybind
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { Component, Show, createMemo, createResource, type JSX } from "solid-js"
|
|
2
|
-
import { createStore } from "solid-js/store"
|
|
3
2
|
import { Button } from "ikanban-ui/button"
|
|
4
3
|
import { Icon } from "ikanban-ui/icon"
|
|
5
4
|
import { Select } from "ikanban-ui/select"
|
|
6
5
|
import { Switch } from "ikanban-ui/switch"
|
|
7
6
|
import { Tooltip } from "ikanban-ui/tooltip"
|
|
8
7
|
import { useTheme, type ColorScheme } from "ikanban-ui/theme"
|
|
9
|
-
import { showToast } from "ikanban-ui/toast"
|
|
10
8
|
import { useLanguage } from "@/context/language"
|
|
11
9
|
import { usePlatform } from "@/context/platform"
|
|
12
10
|
import { useSettings, monoFontFamily } from "@/context/settings"
|
|
@@ -38,69 +36,23 @@ const playDemoSound = (src: string | undefined) => {
|
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
export const SettingsGeneral: Component = () => {
|
|
39
|
+
const CHANGELOG_URL = "https://github.com/isomoes/ikanban/blob/main/CHANGELOG.md"
|
|
40
|
+
|
|
41
41
|
const theme = useTheme()
|
|
42
42
|
const language = useLanguage()
|
|
43
43
|
const platform = usePlatform()
|
|
44
44
|
const settings = useSettings()
|
|
45
45
|
|
|
46
|
-
const [store, setStore] = createStore({
|
|
47
|
-
checking: false,
|
|
48
|
-
})
|
|
49
|
-
|
|
50
46
|
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
|
|
51
47
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
showToast({
|
|
61
|
-
variant: "success",
|
|
62
|
-
icon: "circle-check",
|
|
63
|
-
title: language.t("settings.updates.toast.latest.title"),
|
|
64
|
-
description: language.t("settings.updates.toast.latest.description", { version: platform.version ?? "" }),
|
|
65
|
-
})
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const actions =
|
|
70
|
-
platform.update && platform.restart
|
|
71
|
-
? [
|
|
72
|
-
{
|
|
73
|
-
label: language.t("toast.update.action.installRestart"),
|
|
74
|
-
onClick: async () => {
|
|
75
|
-
await platform.update!()
|
|
76
|
-
await platform.restart!()
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
label: language.t("toast.update.action.notYet"),
|
|
81
|
-
onClick: "dismiss" as const,
|
|
82
|
-
},
|
|
83
|
-
]
|
|
84
|
-
: [
|
|
85
|
-
{
|
|
86
|
-
label: language.t("toast.update.action.notYet"),
|
|
87
|
-
onClick: "dismiss" as const,
|
|
88
|
-
},
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
showToast({
|
|
92
|
-
persistent: true,
|
|
93
|
-
icon: "download",
|
|
94
|
-
title: language.t("toast.update.title"),
|
|
95
|
-
description: language.t("toast.update.description", { version: result.version ?? "" }),
|
|
96
|
-
actions,
|
|
97
|
-
})
|
|
98
|
-
})
|
|
99
|
-
.catch((err: unknown) => {
|
|
100
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
101
|
-
showToast({ title: language.t("common.requestFailed"), description: message })
|
|
102
|
-
})
|
|
103
|
-
.finally(() => setStore("checking", false))
|
|
48
|
+
const openCurrentVersionChanges = () => {
|
|
49
|
+
const normalized = platform.version?.trim().replace(/^v/i, "")
|
|
50
|
+
if (!normalized) {
|
|
51
|
+
platform.openLink(CHANGELOG_URL)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
const fragment = normalized.replace(/\./g, "")
|
|
55
|
+
platform.openLink(`${CHANGELOG_URL}#${fragment}`)
|
|
104
56
|
}
|
|
105
57
|
|
|
106
58
|
const themeOptions = createMemo(() =>
|
|
@@ -412,44 +364,17 @@ export const SettingsGeneral: Component = () => {
|
|
|
412
364
|
</div>
|
|
413
365
|
)
|
|
414
366
|
|
|
415
|
-
const
|
|
367
|
+
const ReleaseNotesSection = () => (
|
|
416
368
|
<div class="flex flex-col gap-1">
|
|
417
|
-
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.
|
|
369
|
+
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.releaseNotes")}</h3>
|
|
418
370
|
|
|
419
371
|
<div class="bg-surface-raised-base px-4 rounded-lg">
|
|
420
372
|
<SettingsRow
|
|
421
|
-
title={language.t("settings.
|
|
422
|
-
description={language.t("settings.
|
|
423
|
-
>
|
|
424
|
-
<div data-action="settings-updates-startup">
|
|
425
|
-
<Switch
|
|
426
|
-
checked={settings.updates.startup()}
|
|
427
|
-
disabled={!platform.checkUpdate}
|
|
428
|
-
onChange={(checked) => settings.updates.setStartup(checked)}
|
|
429
|
-
/>
|
|
430
|
-
</div>
|
|
431
|
-
</SettingsRow>
|
|
432
|
-
|
|
433
|
-
<SettingsRow
|
|
434
|
-
title={language.t("settings.general.row.releaseNotes.title")}
|
|
435
|
-
description={language.t("settings.general.row.releaseNotes.description")}
|
|
373
|
+
title={language.t("settings.general.row.releaseNotesCurrent.title")}
|
|
374
|
+
description={language.t("settings.general.row.releaseNotesCurrent.description")}
|
|
436
375
|
>
|
|
437
|
-
<
|
|
438
|
-
|
|
439
|
-
checked={settings.general.releaseNotes()}
|
|
440
|
-
onChange={(checked) => settings.general.setReleaseNotes(checked)}
|
|
441
|
-
/>
|
|
442
|
-
</div>
|
|
443
|
-
</SettingsRow>
|
|
444
|
-
|
|
445
|
-
<SettingsRow
|
|
446
|
-
title={language.t("settings.updates.row.check.title")}
|
|
447
|
-
description={language.t("settings.updates.row.check.description")}
|
|
448
|
-
>
|
|
449
|
-
<Button size="small" variant="secondary" disabled={store.checking || !platform.checkUpdate} onClick={check}>
|
|
450
|
-
{store.checking
|
|
451
|
-
? language.t("settings.updates.action.checking")
|
|
452
|
-
: language.t("settings.updates.action.checkNow")}
|
|
376
|
+
<Button size="small" variant="secondary" onClick={openCurrentVersionChanges}>
|
|
377
|
+
{language.t("settings.general.action.releaseNotesCurrent")}
|
|
453
378
|
</Button>
|
|
454
379
|
</SettingsRow>
|
|
455
380
|
</div>
|
|
@@ -473,6 +398,8 @@ export const SettingsGeneral: Component = () => {
|
|
|
473
398
|
|
|
474
399
|
<SoundsSection />
|
|
475
400
|
|
|
401
|
+
<ReleaseNotesSection />
|
|
402
|
+
|
|
476
403
|
{/*<Show when={platform.platform === "desktop" && platform.os === "windows" && platform.getWslEnabled}>
|
|
477
404
|
{(_) => {
|
|
478
405
|
const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.())
|
|
@@ -501,8 +428,6 @@ export const SettingsGeneral: Component = () => {
|
|
|
501
428
|
}}
|
|
502
429
|
</Show>*/}
|
|
503
430
|
|
|
504
|
-
<UpdatesSection />
|
|
505
|
-
|
|
506
431
|
<Show when={linux()}>
|
|
507
432
|
{(_) => {
|
|
508
433
|
const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.())
|
|
@@ -72,6 +72,7 @@ const baseState = (input: Partial<State> = {}) =>
|
|
|
72
72
|
sessionTotal: 0,
|
|
73
73
|
session_status: {},
|
|
74
74
|
session_diff: {},
|
|
75
|
+
project_diff: {},
|
|
75
76
|
todo: {},
|
|
76
77
|
permission: {},
|
|
77
78
|
question: {},
|
|
@@ -174,6 +175,7 @@ describe("applyDirectoryEvent", () => {
|
|
|
174
175
|
message: { ses_1: [message] },
|
|
175
176
|
part: { [message.id]: [textPart("prt_1", "ses_1", message.id)] },
|
|
176
177
|
session_diff: { ses_1: [] },
|
|
178
|
+
project_diff: { "/tmp": [] },
|
|
177
179
|
todo: { ses_1: [] },
|
|
178
180
|
permission: { ses_1: [] },
|
|
179
181
|
question: { ses_1: [] },
|
|
@@ -195,6 +197,7 @@ describe("applyDirectoryEvent", () => {
|
|
|
195
197
|
expect(store.message.ses_1).toBeUndefined()
|
|
196
198
|
expect(store.part[message.id]).toBeUndefined()
|
|
197
199
|
expect(store.session_diff.ses_1).toBeUndefined()
|
|
200
|
+
expect(store.project_diff["/tmp"]).toEqual([])
|
|
198
201
|
expect(store.todo.ses_1).toBeUndefined()
|
|
199
202
|
expect(store.permission.ses_1).toBeUndefined()
|
|
200
203
|
expect(store.question.ses_1).toBeUndefined()
|
|
@@ -220,6 +223,7 @@ describe("applyDirectoryEvent", () => {
|
|
|
220
223
|
message: { [item.info.id]: [message] },
|
|
221
224
|
part: { [message.id]: [textPart("prt_1", item.info.id, message.id)] },
|
|
222
225
|
session_diff: { [item.info.id]: [] },
|
|
226
|
+
project_diff: { "/tmp": [] },
|
|
223
227
|
todo: { [item.info.id]: [] },
|
|
224
228
|
permission: { [item.info.id]: [] },
|
|
225
229
|
question: { [item.info.id]: [] },
|
|
@@ -241,6 +245,7 @@ describe("applyDirectoryEvent", () => {
|
|
|
241
245
|
expect(store.message[item.info.id]).toBeUndefined()
|
|
242
246
|
expect(store.part[message.id]).toBeUndefined()
|
|
243
247
|
expect(store.session_diff[item.info.id]).toBeUndefined()
|
|
248
|
+
expect(store.project_diff["/tmp"]).toEqual([])
|
|
244
249
|
expect(store.todo[item.info.id]).toBeUndefined()
|
|
245
250
|
expect(store.permission[item.info.id]).toBeUndefined()
|
|
246
251
|
expect(store.question[item.info.id]).toBeUndefined()
|