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.
- package/dist/assets/{ghostty-web-BnnMBzi5.js → ghostty-web--axxBQj6.js} +1 -1
- package/dist/assets/{home-DeLBKxk3.js → home-BAEIhduF.js} +1 -1
- package/dist/assets/{index-pnX-hTQP.css → index-B0TEjAod.css} +1 -1
- package/dist/assets/{index-ehO8js0v.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/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/sync.tsx +237 -2
- package/src/i18n/en.ts +11 -0
- package/src/i18n/zh.ts +22 -0
- package/src/pages/layout.tsx +11 -54
- package/src/pages/session/review-tab.tsx +3 -1
- package/src/pages/session/session-side-panel.tsx +17 -51
- package/src/pages/session/use-session-commands.tsx +91 -33
- package/src/pages/session.tsx +87 -16
- package/dist/assets/session-D7vMISoz.js +0 -24
|
@@ -1,208 +1,36 @@
|
|
|
1
|
-
import { createEffect, createSignal
|
|
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 = (
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
package/src/context/sync.tsx
CHANGED
|
@@ -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 启动时自动检查更新",
|