ikanban-web 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,208 +1,36 @@
1
- import { createEffect, createSignal, onCleanup } from "solid-js"
1
+ import { createEffect, createSignal } from "solid-js"
2
2
  import { createStore } from "solid-js/store"
3
3
  import { createSimpleContext } from "ikanban-ui/context"
4
- import { useDialog } from "ikanban-ui/context/dialog"
5
4
  import { usePlatform } from "@/context/platform"
6
- import { useSettings } from "@/context/settings"
7
5
  import { persisted } from "@/utils/persist"
8
- import { DialogReleaseNotes, type Highlight } from "@/components/dialog-release-notes"
9
-
10
- const CHANGELOG_URL = "https://opencode.ai/changelog.json"
11
6
 
12
7
  type Store = {
13
8
  version?: string
14
9
  }
15
10
 
16
- type ParsedRelease = {
17
- tag?: string
18
- highlights: Highlight[]
19
- }
20
-
21
- function isRecord(value: unknown): value is Record<string, unknown> {
22
- return typeof value === "object" && value !== null && !Array.isArray(value)
23
- }
24
-
25
- function getText(value: unknown): string | undefined {
26
- if (typeof value === "string") {
27
- const text = value.trim()
28
- return text.length > 0 ? text : undefined
29
- }
30
-
31
- if (typeof value === "number") return String(value)
32
- return
33
- }
34
-
35
- function normalizeVersion(value: string | undefined) {
36
- const text = value?.trim()
37
- if (!text) return
38
- return text.startsWith("v") || text.startsWith("V") ? text.slice(1) : text
39
- }
40
-
41
- function parseMedia(value: unknown, alt: string): Highlight["media"] | undefined {
42
- if (!isRecord(value)) return
43
- const type = getText(value.type)?.toLowerCase()
44
- const src = getText(value.src) ?? getText(value.url)
45
- if (!src) return
46
- if (type !== "image" && type !== "video") return
47
-
48
- return { type, src, alt }
49
- }
50
-
51
- function parseHighlight(value: unknown): Highlight | undefined {
52
- if (!isRecord(value)) return
53
-
54
- const title = getText(value.title)
55
- if (!title) return
56
-
57
- const description = getText(value.description) ?? getText(value.shortDescription)
58
- if (!description) return
59
-
60
- const media = parseMedia(value.media, title)
61
- return { title, description, media }
62
- }
63
-
64
- function parseRelease(value: unknown): ParsedRelease | undefined {
65
- if (!isRecord(value)) return
66
- const tag = getText(value.tag) ?? getText(value.tag_name) ?? getText(value.name)
67
-
68
- if (!Array.isArray(value.highlights)) {
69
- return { tag, highlights: [] }
70
- }
71
-
72
- const highlights = value.highlights.flatMap((group) => {
73
- if (!isRecord(group)) return []
74
-
75
- const source = getText(group.source)
76
- if (!source) return []
77
- if (!source.toLowerCase().includes("desktop")) return []
78
-
79
- if (Array.isArray(group.items)) {
80
- return group.items.map((item) => parseHighlight(item)).filter((item): item is Highlight => item !== undefined)
81
- }
82
-
83
- const item = parseHighlight(group)
84
- if (!item) return []
85
- return [item]
86
- })
87
-
88
- return { tag, highlights }
89
- }
90
-
91
- function parseChangelog(value: unknown): ParsedRelease[] | undefined {
92
- if (Array.isArray(value)) {
93
- return value.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined)
94
- }
95
-
96
- if (!isRecord(value)) return
97
- if (!Array.isArray(value.releases)) return
98
-
99
- return value.releases.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined)
100
- }
101
-
102
- function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; previous?: string }) {
103
- const current = normalizeVersion(input.current)
104
- const previous = normalizeVersion(input.previous)
105
- const releases = input.releases
106
-
107
- const start = (() => {
108
- if (!current) return 0
109
- const index = releases.findIndex((release) => normalizeVersion(release.tag) === current)
110
- return index === -1 ? 0 : index
111
- })()
112
-
113
- const end = (() => {
114
- if (!previous) return releases.length
115
- const index = releases.findIndex((release, i) => i >= start && normalizeVersion(release.tag) === previous)
116
- return index === -1 ? releases.length : index
117
- })()
118
-
119
- const highlights = releases.slice(start, end).flatMap((release) => release.highlights)
120
- const seen = new Set<string>()
121
- const unique = highlights.filter((highlight) => {
122
- const key = dedupeKey(highlight)
123
- if (seen.has(key)) return false
124
- seen.add(key)
125
- return true
126
- })
127
- return unique.slice(0, 5)
128
- }
129
-
130
- function dedupeKey(highlight: Highlight) {
131
- return [highlight.title, highlight.description, highlight.media?.type ?? "", highlight.media?.src ?? ""].join("\n")
132
- }
133
-
134
- function loadReleaseHighlights(value: unknown, current?: string, previous?: string) {
135
- const releases = parseChangelog(value)
136
- if (!releases?.length) return []
137
- return sliceHighlights({ releases, current, previous })
138
- }
139
-
140
11
  export const { use: useHighlights, provider: HighlightsProvider } = createSimpleContext({
141
12
  name: "Highlights",
142
13
  gate: false,
143
14
  init: () => {
144
15
  const platform = usePlatform()
145
- const dialog = useDialog()
146
- const settings = useSettings()
147
16
  const [store, setStore, _, ready] = persisted("highlights.v1", createStore<Store>({ version: undefined }))
148
17
 
149
18
  const [from, setFrom] = createSignal<string | undefined>(undefined)
150
19
  const [to, setTo] = createSignal<string | undefined>(undefined)
151
20
  const state = { started: false }
152
- let timer: ReturnType<typeof setTimeout> | undefined
153
-
154
- const clearTimer = () => {
155
- if (timer === undefined) return
156
- clearTimeout(timer)
157
- timer = undefined
158
- }
159
-
160
21
  const markSeen = () => {
161
22
  if (!platform.version) return
162
23
  setStore("version", platform.version)
163
24
  }
164
25
 
165
- const start = (previous: string) => {
166
- if (!settings.general.releaseNotes()) {
167
- markSeen()
168
- return
169
- }
170
-
171
- const fetcher = platform.fetch ?? fetch
172
- const controller = new AbortController()
173
- onCleanup(() => {
174
- controller.abort()
175
- clearTimer()
176
- })
177
-
178
- fetcher(CHANGELOG_URL, {
179
- signal: controller.signal,
180
- headers: { Accept: "application/json" },
181
- })
182
- .then((response) => (response.ok ? (response.json() as Promise<unknown>) : undefined))
183
- .then((json) => {
184
- if (!json) return
185
- const highlights = loadReleaseHighlights(json, platform.version, previous)
186
- if (controller.signal.aborted) return
187
-
188
- if (highlights.length === 0) {
189
- markSeen()
190
- return
191
- }
192
-
193
- timer = setTimeout(() => {
194
- timer = undefined
195
- markSeen()
196
- dialog.show(() => <DialogReleaseNotes highlights={highlights} />)
197
- }, 500)
198
- })
199
- .catch(() => undefined)
26
+ const start = (_previous: string) => {
27
+ // Updates and release-note prompts are temporarily disabled.
28
+ markSeen()
200
29
  }
201
30
 
202
31
  createEffect(() => {
203
32
  if (state.started) return
204
33
  if (!ready()) return
205
- if (!settings.ready()) return
206
34
  if (!platform.version) return
207
35
  state.started = true
208
36
 
@@ -1,11 +1,180 @@
1
1
  import { batch, createMemo } from "solid-js"
2
- import { createStore, produce, reconcile } from "solid-js/store"
2
+ import { createStore, produce, reconcile, type SetStoreFunction } from "solid-js/store"
3
3
  import { Binary } from "ikanban-utils/binary"
4
4
  import { retry } from "ikanban-utils/retry"
5
5
  import { createSimpleContext } from "ikanban-ui/context"
6
+ import { applyPatch, parsePatch, reversePatch, type StructuredPatch } from "diff"
6
7
  import { useGlobalSync } from "./global-sync"
7
8
  import { useSDK } from "./sdk"
8
- import type { Message, Part } from "@opencode-ai/sdk/v2/client"
9
+ import type { File as SDKFile, FileContent, FileDiff, Message, Part } from "@opencode-ai/sdk/v2/client"
10
+
11
+ type ProjectDiffEntry = FileDiff & {
12
+ lazy?: boolean
13
+ loading?: boolean
14
+ binary?: boolean
15
+ }
16
+
17
+ const PROJECT_DIFF_INITIAL_READ_LIMIT = 20
18
+ const PROJECT_DIFF_READ_CONCURRENCY = 4
19
+ const PROJECT_DIFF_MAX_EAGER_CHANGED_LINES = 400
20
+ const BINARY_FILE_RE = /\.(?:png|jpe?g|gif|webp|bmp|ico|avif|mp3|wav|ogg|flac|mp4|mov|avi|mkv|webm|pdf|zip|gz|tar|7z|woff2?|ttf|eot|otf|exe|bin|so|dylib|dll|class|jar|wasm)$/i
21
+
22
+ function isLikelyBinaryPath(path: string) {
23
+ return BINARY_FILE_RE.test(path)
24
+ }
25
+
26
+ function createProjectPlaceholder(file: SDKFile, input: { lazy?: boolean; loading?: boolean; binary?: boolean } = {}): ProjectDiffEntry {
27
+ return {
28
+ file: file.path,
29
+ before: "",
30
+ after: "",
31
+ additions: file.added,
32
+ deletions: file.removed,
33
+ status: file.status,
34
+ lazy: input.lazy,
35
+ loading: input.loading,
36
+ binary: input.binary,
37
+ }
38
+ }
39
+
40
+ function shouldEagerLoadProjectDiff(file: SDKFile) {
41
+ if (isLikelyBinaryPath(file.path)) return false
42
+ if (file.added + file.removed > PROJECT_DIFF_MAX_EAGER_CHANGED_LINES) return false
43
+ return true
44
+ }
45
+
46
+ function getStructuredPatch(content?: FileContent): StructuredPatch | undefined {
47
+ if (!content) return
48
+ if (content.diff) {
49
+ const parsed = parsePatch(content.diff)[0]
50
+ if (parsed) return parsed
51
+ }
52
+ if (!content.patch) return
53
+ return {
54
+ oldFileName: content.patch.oldFileName,
55
+ newFileName: content.patch.newFileName,
56
+ oldHeader: content.patch.oldHeader,
57
+ newHeader: content.patch.newHeader,
58
+ hunks: content.patch.hunks.map((hunk) => ({
59
+ oldStart: hunk.oldStart,
60
+ oldLines: hunk.oldLines,
61
+ newStart: hunk.newStart,
62
+ newLines: hunk.newLines,
63
+ lines: [...hunk.lines],
64
+ })),
65
+ }
66
+ }
67
+
68
+ function buildWorkspaceDiff(file: SDKFile, content?: FileContent): ProjectDiffEntry | undefined {
69
+ const patch = getStructuredPatch(content)
70
+
71
+ if (content?.type === "binary") {
72
+ return createProjectPlaceholder(file, { binary: true })
73
+ }
74
+
75
+ if (file.status === "added") {
76
+ const after = content?.content ?? (patch ? applyPatch("", patch) || "" : "")
77
+ return {
78
+ file: file.path,
79
+ before: "",
80
+ after,
81
+ additions: file.added,
82
+ deletions: file.removed,
83
+ status: file.status,
84
+ }
85
+ }
86
+
87
+ if (file.status === "deleted") {
88
+ const before = content?.content ?? (patch ? applyPatch("", reversePatch(patch)) || "" : "")
89
+ return {
90
+ file: file.path,
91
+ before,
92
+ after: "",
93
+ additions: file.added,
94
+ deletions: file.removed,
95
+ status: file.status,
96
+ }
97
+ }
98
+
99
+ const after = content?.content ?? ""
100
+ const before = patch ? applyPatch(after, reversePatch(patch)) || after : after
101
+
102
+ if (!patch && file.added + file.removed === 0) return
103
+
104
+ return {
105
+ file: file.path,
106
+ before,
107
+ after,
108
+ additions: file.added,
109
+ deletions: file.removed,
110
+ status: file.status,
111
+ }
112
+ }
113
+
114
+ async function fetchProjectDiffs(client: ReturnType<typeof useSDK>["client"], directory: string): Promise<FileDiff[]> {
115
+ const files = await retry(() => client.file.status({ directory })).then((result) => result.data ?? [])
116
+ return files.map((file) =>
117
+ createProjectPlaceholder(file, {
118
+ lazy: !shouldEagerLoadProjectDiff(file),
119
+ loading: false,
120
+ }),
121
+ )
122
+ }
123
+
124
+ async function hydrateProjectDiffFile(
125
+ client: ReturnType<typeof useSDK>["client"],
126
+ directory: string,
127
+ file: SDKFile,
128
+ ): Promise<ProjectDiffEntry> {
129
+ const content = await retry(() => client.file.read({ path: file.path, directory }))
130
+ .then((result) => result.data)
131
+ .catch(() => undefined)
132
+ return buildWorkspaceDiff(file, content) ?? createProjectPlaceholder(file)
133
+ }
134
+
135
+ function updateProjectDiffEntry(
136
+ setStore: SetStoreFunction<any>,
137
+ directory: string,
138
+ path: string,
139
+ updater: (current: ProjectDiffEntry) => ProjectDiffEntry,
140
+ ) {
141
+ setStore(
142
+ "project_diff",
143
+ directory,
144
+ produce((draft: ProjectDiffEntry[]) => {
145
+ const index = draft.findIndex((item) => item.file === path)
146
+ if (index === -1) return
147
+ draft[index] = updater(draft[index])
148
+ }),
149
+ )
150
+ }
151
+
152
+ async function hydrateProjectDiffBatch(input: {
153
+ client: ReturnType<typeof useSDK>["client"]
154
+ directory: string
155
+ setStore: SetStoreFunction<any>
156
+ entries: ProjectDiffEntry[]
157
+ }) {
158
+ for (let i = 0; i < input.entries.length; i += PROJECT_DIFF_READ_CONCURRENCY) {
159
+ const chunk = input.entries.slice(i, i + PROJECT_DIFF_READ_CONCURRENCY)
160
+ await Promise.all(
161
+ chunk.map(async (entry) => {
162
+ const file: SDKFile = {
163
+ path: entry.file,
164
+ added: entry.additions,
165
+ removed: entry.deletions,
166
+ status: entry.status ?? "modified",
167
+ }
168
+ const hydrated = await hydrateProjectDiffFile(input.client, input.directory, file)
169
+ updateProjectDiffEntry(input.setStore, input.directory, entry.file, () => ({
170
+ ...hydrated,
171
+ lazy: false,
172
+ loading: false,
173
+ }))
174
+ }),
175
+ )
176
+ }
177
+ }
9
178
 
10
179
  function sortParts(parts: Part[]) {
11
180
  return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
@@ -107,6 +276,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
107
276
  const messagePageSize = 200
108
277
  const inflight = new Map<string, Promise<void>>()
109
278
  const inflightDiff = new Map<string, Promise<void>>()
279
+ const inflightProjectFile = new Map<string, Promise<void>>()
110
280
  const inflightTodo = new Map<string, Promise<void>>()
111
281
  const [meta, setMeta] = createStore({
112
282
  limit: {} as Record<string, number>,
@@ -350,6 +520,71 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
350
520
  )
351
521
  },
352
522
  },
523
+ projectDiff: {
524
+ async diff() {
525
+ const directory = sdk.directory
526
+ const client = sdk.client
527
+ const [store, setStore] = globalSync.child(directory)
528
+ if (store.project_diff[directory] !== undefined) return
529
+
530
+ const key = `project\n${directory}`
531
+ return runInflight(inflightDiff, key, async () => {
532
+ const diff = (await fetchProjectDiffs(client, directory)) as ProjectDiffEntry[]
533
+ const eagerFiles = new Set(
534
+ diff
535
+ .filter((entry) => !entry.lazy)
536
+ .slice(0, PROJECT_DIFF_INITIAL_READ_LIMIT)
537
+ .map((entry) => entry.file),
538
+ )
539
+ const seeded = diff.map((entry) => ({
540
+ ...entry,
541
+ lazy: entry.lazy || !eagerFiles.has(entry.file),
542
+ loading: eagerFiles.has(entry.file),
543
+ }))
544
+ setStore("project_diff", directory, reconcile(seeded, { key: "file" }))
545
+
546
+ const eager = seeded.filter((entry) => eagerFiles.has(entry.file))
547
+
548
+ if (!eager.length) return
549
+
550
+ await hydrateProjectDiffBatch({
551
+ client,
552
+ directory,
553
+ setStore,
554
+ entries: eager,
555
+ })
556
+ })
557
+ },
558
+ hydrate(path: string) {
559
+ const directory = sdk.directory
560
+ const client = sdk.client
561
+ const [store, setStore] = globalSync.child(directory)
562
+ const entries = store.project_diff[directory] as ProjectDiffEntry[] | undefined
563
+ const existing = entries?.find((item) => item.file === path)
564
+ if (!existing || (!existing.lazy && !existing.loading)) return
565
+
566
+ const fileKey = `project-file\n${directory}\n${path}`
567
+ return runInflight(inflightProjectFile, fileKey, async () => {
568
+ updateProjectDiffEntry(setStore, directory, path, (current) => ({
569
+ ...current,
570
+ loading: true,
571
+ }))
572
+
573
+ const file: SDKFile = {
574
+ path: existing.file,
575
+ added: existing.additions,
576
+ removed: existing.deletions,
577
+ status: existing.status ?? "modified",
578
+ }
579
+ const hydrated = await hydrateProjectDiffFile(client, directory, file)
580
+ updateProjectDiffEntry(setStore, directory, path, () => ({
581
+ ...hydrated,
582
+ lazy: false,
583
+ loading: false,
584
+ }))
585
+ })
586
+ },
587
+ },
353
588
  absolute,
354
589
  get directory() {
355
590
  return current()[0].path.directory
package/src/i18n/en.ts CHANGED
@@ -23,6 +23,7 @@ export const dict = {
23
23
 
24
24
  "command.sidebar.toggle": "Toggle sidebar",
25
25
  "command.project.open": "Open project",
26
+ "command.project.close": "Close project",
26
27
  "command.provider.connect": "Connect provider",
27
28
  "command.server.switch": "Switch server",
28
29
  "command.settings.open": "Open settings",
@@ -77,6 +78,8 @@ export const dict = {
77
78
  "command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar",
78
79
  "command.session.undo": "Undo",
79
80
  "command.session.undo.description": "Undo the last message",
81
+ "command.session.timeline": "Timeline",
82
+ "command.session.timeline.description": "Jump to any message in this session timeline",
80
83
  "command.session.redo": "Redo",
81
84
  "command.session.redo.description": "Redo the last undone message",
82
85
  "command.session.compact": "Compact session",
@@ -303,6 +306,8 @@ export const dict = {
303
306
  "mcp.status.disabled": "disabled",
304
307
 
305
308
  "dialog.fork.empty": "No messages to fork from",
309
+ "dialog.timeline.empty": "No messages in this timeline",
310
+ "dialog.timeline.latest": "Latest message",
306
311
 
307
312
  "dialog.directory.search.placeholder": "Search folders",
308
313
  "dialog.directory.empty": "No folders found",
@@ -513,6 +518,8 @@ export const dict = {
513
518
  "session.review.loadingChanges": "Loading changes...",
514
519
  "session.review.empty": "No changes in this session yet",
515
520
  "session.review.noVcs": "No git VCS detected, so session changes will not be detected",
521
+ "session.review.emptyProject": "No project changes yet",
522
+ "session.review.noVcsProject": "No git VCS detected, so project changes will not be detected",
516
523
  "session.review.noChanges": "No changes",
517
524
 
518
525
  "session.files.selectToOpen": "Select a file to open",
@@ -624,6 +631,7 @@ export const dict = {
624
631
  "settings.general.section.notifications": "System notifications",
625
632
  "settings.general.section.updates": "Updates",
626
633
  "settings.general.section.sounds": "Sound effects",
634
+ "settings.general.section.releaseNotes": "Release notes",
627
635
  "settings.general.section.feed": "Feed",
628
636
  "settings.general.section.display": "Display",
629
637
 
@@ -651,6 +659,9 @@ export const dict = {
651
659
 
652
660
  "settings.general.row.releaseNotes.title": "Release notes",
653
661
  "settings.general.row.releaseNotes.description": "Show What's New popups after updates",
662
+ "settings.general.row.releaseNotesCurrent.title": "Current version changes",
663
+ "settings.general.row.releaseNotesCurrent.description": "Open this version's changelog entries on GitHub",
664
+ "settings.general.action.releaseNotesCurrent": "View changes",
654
665
 
655
666
  "settings.updates.row.startup.title": "Check for updates on startup",
656
667
  "settings.updates.row.startup.description": "Automatically check for updates when OpenCode launches",
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": "所有文件",
@@ -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 启动时自动检查更新",