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/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>OpenCode</title>
7
- <link rel="icon" type="image/png" href="/ikanban/favicon-96x96-v3.png" sizes="96x96" />
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 rel="apple-touch-icon" sizes="180x180" href="/ikanban/apple-touch-icon-v3.png" />
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 name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
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-ehO8js0v.js"></script>
18
- <link rel="stylesheet" crossorigin href="/ikanban/assets/index-pnX-hTQP.css">
30
+ <script type="module" crossorigin src="/ikanban/assets/index-BlcL7wgr.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikanban-web",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "iKanban web app",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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 diffs = sync.data.session_diff[sessionID]
166
- if (!diffs) return false
167
- return diffs.some((diff) => diff.file === path)
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" }) => {
@@ -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 check = () => {
53
- if (!platform.checkUpdate) return
54
- setStore("checking", true)
55
-
56
- void platform
57
- .checkUpdate()
58
- .then((result) => {
59
- if (!result.updateAvailable) {
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 UpdatesSection = () => (
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.updates")}</h3>
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.updates.row.startup.title")}
422
- description={language.t("settings.updates.row.startup.description")}
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
- <div data-action="settings-release-notes">
438
- <Switch
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?.())
@@ -169,6 +169,7 @@ export function createChildStoreManager(input: {
169
169
  sessionTotal: 0,
170
170
  session_status: {},
171
171
  session_diff: {},
172
+ project_diff: {},
172
173
  todo: {},
173
174
  permission: {},
174
175
  question: {},
@@ -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()