opencode-extended-sidebar 0.2.90 → 0.2.91
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/.oesignore +17 -17
- package/CHANGELOG.md +96 -95
- package/LICENSE +21 -21
- package/README.md +150 -235
- package/oes.json +13 -11
- package/package.json +100 -100
- package/scripts/install-git-hooks.mjs +29 -29
- package/src/pware.oc.core/constants/index.ts +18 -18
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -29
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +15 -13
- package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -241
- package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -55
- package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -65
- package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -58
- package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -63
- package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -34
- package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -74
- package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -69
- package/src/pware.oc.core/git/index.ts +7 -7
- package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -237
- package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -87
- package/src/pware.oc.core/index.ts +19 -18
- package/src/pware.oc.core/pware.oc.core.bus.ts +42 -42
- package/src/pware.oc.core/pware.oc.core.cache.ts +48 -48
- package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -97
- package/src/pware.oc.core/pware.oc.core.debug.ts +280 -280
- package/src/pware.oc.core/pware.oc.core.events.ts +79 -79
- package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -135
- package/src/pware.oc.core/pware.oc.core.layout.ts +234 -234
- package/src/pware.oc.core/pware.oc.core.oes.ts +99 -89
- package/src/pware.oc.core/pware.oc.core.paths.ts +240 -240
- package/src/pware.oc.core/pware.oc.core.preview.ts +74 -74
- package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -726
- package/src/pware.oc.core/pware.oc.core.runtimeConfig.ts +41 -0
- package/src/pware.oc.core/pware.oc.core.sqlite.ts +164 -175
- package/src/pware.oc.core/pware.oc.core.sqliteGateway.ts +89 -0
- package/src/pware.oc.core/pware.oc.core.status.ts +214 -214
- package/src/pware.oc.core/pware.oc.core.timing.ts +63 -63
- package/src/pware.oc.core/pware.oc.core.width.ts +215 -215
- package/src/pware.oc.omo/constants/index.ts +15 -15
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -18
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -49
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -29
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -11
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -60
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -49
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -25
- package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -38
- package/src/pware.oc.omo/index.ts +7 -7
- package/src/pware.oc.omo/resolver/index.ts +19 -19
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -179
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -116
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -34
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -503
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -40
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -269
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -155
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +363 -361
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -18
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -11
- package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -14
- package/src/pware.oc.opencode/constants/index.ts +10 -10
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -15
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -18
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -45
- package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -58
- package/src/pware.oc.opencode/index.ts +8 -8
- package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -182
- package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -399
- package/src/pware.oc.opencode/resolver/index.ts +223 -170
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +150 -162
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +149 -185
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +248 -211
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +22 -26
- package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +224 -236
- package/src/pware.oc.perf/index.ts +14 -14
- package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -15
- package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -284
- package/src/pware.oc.perf/pware.oc.perf.reader.ts +962 -927
- package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -88
- package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -118
- package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -79
- package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -71
- package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -214
- package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -179
- package/src/pware.oc.perf/pware.oc.perf.view.tsx +463 -462
- package/src/pware.oc.runtime/index.ts +13 -13
- package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -142
- package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +87 -79
- package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +282 -253
- package/src/pware.oc.runtime/pware.oc.runtime.omoRead.ts +37 -0
- package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +83 -95
- package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +130 -129
- package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -105
- package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +44 -43
- package/src/pware.oc.runtime/resolver/index.ts +268 -224
- package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -146
- package/src/pware.oc.ui/index.ts +12 -12
- package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +381 -339
- package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -144
- package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -111
- package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -82
- package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -843
- package/src/pware.oc.ui/pware.oc.ui.sections.tsx +552 -526
- package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1718 -1629
- package/src/pware.oc.ui.tsx +98 -98
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pware.oc.core.constants.toolname
|
|
3
|
-
*
|
|
4
|
-
* OpenCode tool names grouped by file-touch behavior (the plugin's
|
|
5
|
-
* WRITE_TOOLS / READ_TOOLS in files.ts) plus the special non-file tools the
|
|
6
|
-
* code classifies (`bash`, `task`, `question`).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** Tool name that edits an existing file. */
|
|
10
|
-
export const TOOL_EDIT = "edit"
|
|
11
|
-
|
|
12
|
-
/** Tool name that writes a file. */
|
|
13
|
-
export const TOOL_WRITE = "write"
|
|
14
|
-
|
|
15
|
-
/** Tool name for a multi-file edit. */
|
|
16
|
-
export const TOOL_MULTIEDIT = "multiedit"
|
|
17
|
-
|
|
18
|
-
/** Tool name for applying a prebuilt edit (underscore spelling). */
|
|
19
|
-
export const TOOL_APPLY_EDIT = "apply_edit"
|
|
20
|
-
|
|
21
|
-
/** Tool name for applying a prebuilt edit (compact spelling). */
|
|
22
|
-
export const TOOL_APPLYEDIT = "applyedit"
|
|
23
|
-
|
|
24
|
-
/** Tool name that deletes a file. */
|
|
25
|
-
export const TOOL_DELETE = "delete"
|
|
26
|
-
|
|
27
|
-
/** Tool name that removes a file. */
|
|
28
|
-
export const TOOL_REMOVE = "remove"
|
|
29
|
-
|
|
30
|
-
/** Tool name that reads a file. */
|
|
31
|
-
export const TOOL_READ = "read"
|
|
32
|
-
|
|
33
|
-
/** Tool name that views a file. */
|
|
34
|
-
export const TOOL_VIEW = "view"
|
|
35
|
-
|
|
36
|
-
/** Tool name that reads a file (underscore spelling). */
|
|
37
|
-
export const TOOL_READ_FILE = "read_file"
|
|
38
|
-
|
|
39
|
-
/** Tool name that reads a file (compact spelling). */
|
|
40
|
-
export const TOOL_READFILE = "readfile"
|
|
41
|
-
|
|
42
|
-
/** Tool name for a shell command — a non-file tool the panel classifies. */
|
|
43
|
-
export const TOOL_BASH = "bash"
|
|
44
|
-
|
|
45
|
-
/** Tool name for delegating a subtask — a non-file tool the panel classifies. */
|
|
46
|
-
export const TOOL_TASK = "task"
|
|
47
|
-
|
|
48
|
-
/** Tool name for asking the user a question — a non-file tool the panel classifies. */
|
|
49
|
-
export const TOOL_QUESTION = "question"
|
|
50
|
-
|
|
51
|
-
/** Tools that write/delete files — mirrors files.ts WRITE_TOOLS. */
|
|
52
|
-
export const WRITE_TOOLS = [
|
|
53
|
-
TOOL_EDIT,
|
|
54
|
-
TOOL_WRITE,
|
|
55
|
-
TOOL_MULTIEDIT,
|
|
56
|
-
TOOL_APPLY_EDIT,
|
|
57
|
-
TOOL_APPLYEDIT,
|
|
58
|
-
TOOL_DELETE,
|
|
59
|
-
TOOL_REMOVE,
|
|
60
|
-
] as const
|
|
61
|
-
|
|
62
|
-
/** Tools that read/view files — mirrors files.ts READ_TOOLS. */
|
|
63
|
-
export const READ_TOOLS = [TOOL_READ, TOOL_VIEW, TOOL_READ_FILE, TOOL_READFILE] as const
|
|
64
|
-
|
|
65
|
-
/** Non-file tools the panel classifies separately from file-touch behavior. */
|
|
66
|
-
export const NON_FILE_TOOLS = [TOOL_BASH, TOOL_TASK, TOOL_QUESTION] as const
|
|
67
|
-
|
|
68
|
-
/** A tool name the panel classifies by file-touch or special behavior. */
|
|
69
|
-
export type ToolName = (typeof WRITE_TOOLS)[number] | (typeof READ_TOOLS)[number] | (typeof NON_FILE_TOOLS)[number]
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core.constants.toolname
|
|
3
|
+
*
|
|
4
|
+
* OpenCode tool names grouped by file-touch behavior (the plugin's
|
|
5
|
+
* WRITE_TOOLS / READ_TOOLS in files.ts) plus the special non-file tools the
|
|
6
|
+
* code classifies (`bash`, `task`, `question`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Tool name that edits an existing file. */
|
|
10
|
+
export const TOOL_EDIT = "edit"
|
|
11
|
+
|
|
12
|
+
/** Tool name that writes a file. */
|
|
13
|
+
export const TOOL_WRITE = "write"
|
|
14
|
+
|
|
15
|
+
/** Tool name for a multi-file edit. */
|
|
16
|
+
export const TOOL_MULTIEDIT = "multiedit"
|
|
17
|
+
|
|
18
|
+
/** Tool name for applying a prebuilt edit (underscore spelling). */
|
|
19
|
+
export const TOOL_APPLY_EDIT = "apply_edit"
|
|
20
|
+
|
|
21
|
+
/** Tool name for applying a prebuilt edit (compact spelling). */
|
|
22
|
+
export const TOOL_APPLYEDIT = "applyedit"
|
|
23
|
+
|
|
24
|
+
/** Tool name that deletes a file. */
|
|
25
|
+
export const TOOL_DELETE = "delete"
|
|
26
|
+
|
|
27
|
+
/** Tool name that removes a file. */
|
|
28
|
+
export const TOOL_REMOVE = "remove"
|
|
29
|
+
|
|
30
|
+
/** Tool name that reads a file. */
|
|
31
|
+
export const TOOL_READ = "read"
|
|
32
|
+
|
|
33
|
+
/** Tool name that views a file. */
|
|
34
|
+
export const TOOL_VIEW = "view"
|
|
35
|
+
|
|
36
|
+
/** Tool name that reads a file (underscore spelling). */
|
|
37
|
+
export const TOOL_READ_FILE = "read_file"
|
|
38
|
+
|
|
39
|
+
/** Tool name that reads a file (compact spelling). */
|
|
40
|
+
export const TOOL_READFILE = "readfile"
|
|
41
|
+
|
|
42
|
+
/** Tool name for a shell command — a non-file tool the panel classifies. */
|
|
43
|
+
export const TOOL_BASH = "bash"
|
|
44
|
+
|
|
45
|
+
/** Tool name for delegating a subtask — a non-file tool the panel classifies. */
|
|
46
|
+
export const TOOL_TASK = "task"
|
|
47
|
+
|
|
48
|
+
/** Tool name for asking the user a question — a non-file tool the panel classifies. */
|
|
49
|
+
export const TOOL_QUESTION = "question"
|
|
50
|
+
|
|
51
|
+
/** Tools that write/delete files — mirrors files.ts WRITE_TOOLS. */
|
|
52
|
+
export const WRITE_TOOLS = [
|
|
53
|
+
TOOL_EDIT,
|
|
54
|
+
TOOL_WRITE,
|
|
55
|
+
TOOL_MULTIEDIT,
|
|
56
|
+
TOOL_APPLY_EDIT,
|
|
57
|
+
TOOL_APPLYEDIT,
|
|
58
|
+
TOOL_DELETE,
|
|
59
|
+
TOOL_REMOVE,
|
|
60
|
+
] as const
|
|
61
|
+
|
|
62
|
+
/** Tools that read/view files — mirrors files.ts READ_TOOLS. */
|
|
63
|
+
export const READ_TOOLS = [TOOL_READ, TOOL_VIEW, TOOL_READ_FILE, TOOL_READFILE] as const
|
|
64
|
+
|
|
65
|
+
/** Non-file tools the panel classifies separately from file-touch behavior. */
|
|
66
|
+
export const NON_FILE_TOOLS = [TOOL_BASH, TOOL_TASK, TOOL_QUESTION] as const
|
|
67
|
+
|
|
68
|
+
/** A tool name the panel classifies by file-touch or special behavior. */
|
|
69
|
+
export type ToolName = (typeof WRITE_TOOLS)[number] | (typeof READ_TOOLS)[number] | (typeof NON_FILE_TOOLS)[number]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pware.oc.core.git
|
|
3
|
-
*
|
|
4
|
-
* Read-only git status + ignore rules for the Files panel.
|
|
5
|
-
*/
|
|
6
|
-
export * from "./pware.oc.core.git.js"
|
|
7
|
-
export * from "./pware.oc.core.gitignore.js"
|
|
1
|
+
/**
|
|
2
|
+
* pware.oc.core.git
|
|
3
|
+
*
|
|
4
|
+
* Read-only git status + ignore rules for the Files panel.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./pware.oc.core.git.js"
|
|
7
|
+
export * from "./pware.oc.core.gitignore.js"
|
|
@@ -1,237 +1,237 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read-only git status for Files letters. No repo / no git → empty, never throws.
|
|
3
|
-
* Status is scoped to the listed paths — never the whole worktree.
|
|
4
|
-
* Spawn is async: the TUI keeps the last letters until git returns.
|
|
5
|
-
*/
|
|
6
|
-
import { spawn } from "node:child_process"
|
|
7
|
-
import fs from "node:fs"
|
|
8
|
-
import path from "node:path"
|
|
9
|
-
import { fileStamp } from "../pware.oc.core.paths.js"
|
|
10
|
-
|
|
11
|
-
/** Porcelain XY letters only. `?` is untracked. Do not invent extras here. */
|
|
12
|
-
export type GitLetter = "M" | "A" | "D" | "R" | "C" | "U" | "T" | "?"
|
|
13
|
-
|
|
14
|
-
const GIT_PATH_CAP = 40
|
|
15
|
-
const GIT_DEBOUNCE_MS = 2500
|
|
16
|
-
const GIT_TIMEOUT_MS = 1500
|
|
17
|
-
|
|
18
|
-
export function findGitRoot(start?: string | null): string | null {
|
|
19
|
-
if (!start) return null
|
|
20
|
-
let dir = path.resolve(start)
|
|
21
|
-
for (let i = 0; i < 16; i++) {
|
|
22
|
-
try {
|
|
23
|
-
if (fs.existsSync(path.join(dir, ".git"))) return dir
|
|
24
|
-
} catch {
|
|
25
|
-
return null
|
|
26
|
-
}
|
|
27
|
-
const parent = path.dirname(dir)
|
|
28
|
-
if (parent === dir) break
|
|
29
|
-
dir = parent
|
|
30
|
-
}
|
|
31
|
-
return null
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function gitStatusStamp(projectRoot?: string | null): string {
|
|
35
|
-
const root = findGitRoot(projectRoot)
|
|
36
|
-
if (!root) return "0"
|
|
37
|
-
return [fileStamp(path.join(root, ".git")), fileStamp(path.join(root, ".git", "HEAD")), fileStamp(path.join(root, ".git", "index"))].join("|")
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function porcelainLetter(xy: string): GitLetter | null {
|
|
41
|
-
if (xy === "??") return "?"
|
|
42
|
-
if (xy === "!!") return null
|
|
43
|
-
const x = xy[0] ?? " "
|
|
44
|
-
const y = xy[1] ?? " "
|
|
45
|
-
if (x === "U" || y === "U" || xy === "AA" || xy === "DD") return "U"
|
|
46
|
-
const rank = (c: string): GitLetter | null => {
|
|
47
|
-
if (c === "D") return "D"
|
|
48
|
-
if (c === "R") return "R"
|
|
49
|
-
if (c === "C") return "C"
|
|
50
|
-
if (c === "A") return "A"
|
|
51
|
-
if (c === "T") return "T"
|
|
52
|
-
if (c === "M") return "M"
|
|
53
|
-
return null
|
|
54
|
-
}
|
|
55
|
-
return rank(x) ?? rank(y)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function xIsRename(x: string): boolean {
|
|
59
|
-
return x === "R" || x === "C"
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Parse `git status --porcelain -z` stdout. Exported for tests. */
|
|
63
|
-
export function parsePorcelainZ(buf: string): Map<string, GitLetter> {
|
|
64
|
-
const out = new Map<string, GitLetter>()
|
|
65
|
-
let i = 0
|
|
66
|
-
while (i < buf.length) {
|
|
67
|
-
const xy = buf.slice(i, i + 2)
|
|
68
|
-
if (xy.length < 2) break
|
|
69
|
-
i += 2
|
|
70
|
-
if (buf[i] === " ") i += 1
|
|
71
|
-
const z = buf.indexOf("\0", i)
|
|
72
|
-
if (z < 0) break
|
|
73
|
-
const p1 = buf.slice(i, z).replace(/\\/g, "/")
|
|
74
|
-
i = z + 1
|
|
75
|
-
if (xIsRename(xy[0] ?? "")) {
|
|
76
|
-
const z2 = buf.indexOf("\0", i)
|
|
77
|
-
if (z2 >= 0) i = z2 + 1
|
|
78
|
-
}
|
|
79
|
-
const letter = porcelainLetter(xy)
|
|
80
|
-
if (letter && p1) out.set(p1.toLowerCase(), letter)
|
|
81
|
-
}
|
|
82
|
-
return out
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function relToGitRoot(posixPath: string, root: string): string {
|
|
86
|
-
const file = posixPath.replace(/\\/g, "/")
|
|
87
|
-
const base = root.replace(/\\/g, "/").replace(/\/+$/, "")
|
|
88
|
-
const lower = file.toLowerCase()
|
|
89
|
-
const rootLower = base.toLowerCase()
|
|
90
|
-
if (lower.startsWith(`${rootLower}/`)) return lower.slice(rootLower.length + 1)
|
|
91
|
-
return lower.replace(/^\.\//, "")
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Pathspecs for `git status`: repo paths only, deduped, capped. Exported for tests. */
|
|
95
|
-
export function relsFrom(posixPaths: string[], root: string): string[] {
|
|
96
|
-
const base = root.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase()
|
|
97
|
-
const seen = new Set<string>()
|
|
98
|
-
const out: string[] = []
|
|
99
|
-
for (const p of posixPaths) {
|
|
100
|
-
const file = p.replace(/\\/g, "/")
|
|
101
|
-
// git aborts the whole status with exit 128 on a single outside-repo
|
|
102
|
-
// pathspec — sessions touch temp/config files outside the tree all the time.
|
|
103
|
-
if (!file.toLowerCase().startsWith(`${base}/`)) continue
|
|
104
|
-
const rel = relToGitRoot(p, root)
|
|
105
|
-
if (!rel || seen.has(rel)) continue
|
|
106
|
-
seen.add(rel)
|
|
107
|
-
out.push(rel)
|
|
108
|
-
if (out.length >= GIT_PATH_CAP) break
|
|
109
|
-
}
|
|
110
|
-
return out
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let cacheKey = ""
|
|
114
|
-
let cacheMarks = new Map<string, GitLetter>()
|
|
115
|
-
let cacheRoot: string | null = null
|
|
116
|
-
let lastPathKey = ""
|
|
117
|
-
let lastSpawn = 0
|
|
118
|
-
let pendingKey = ""
|
|
119
|
-
let spawnGen = 0
|
|
120
|
-
let inFlight = false
|
|
121
|
-
|
|
122
|
-
const listeners = new Set<() => void>()
|
|
123
|
-
|
|
124
|
-
function notify(): void {
|
|
125
|
-
for (const fn of listeners) {
|
|
126
|
-
try {
|
|
127
|
-
fn()
|
|
128
|
-
} catch {
|
|
129
|
-
// sidebar teardown
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Re-render when an async git status lands. Returns unsubscribe. */
|
|
135
|
-
export function onGitMarksChange(fn: () => void): () => void {
|
|
136
|
-
listeners.add(fn)
|
|
137
|
-
return () => {
|
|
138
|
-
listeners.delete(fn)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Drop cached letters and in-flight spawn accounting (tests). */
|
|
143
|
-
export function resetGitCache(): void {
|
|
144
|
-
spawnGen += 1
|
|
145
|
-
inFlight = false
|
|
146
|
-
cacheKey = ""
|
|
147
|
-
pendingKey = ""
|
|
148
|
-
lastPathKey = ""
|
|
149
|
-
lastSpawn = 0
|
|
150
|
-
cacheRoot = null
|
|
151
|
-
cacheMarks = new Map()
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function runGit(root: string, rels: string[], key: string, gen: number): void {
|
|
155
|
-
let child: ReturnType<typeof spawn>
|
|
156
|
-
try {
|
|
157
|
-
child = spawn(
|
|
158
|
-
"git",
|
|
159
|
-
["-c", "core.quotepath=false", "status", "--porcelain", "-z", "--", ...rels],
|
|
160
|
-
{ cwd: root, windowsHide: true },
|
|
161
|
-
)
|
|
162
|
-
} catch {
|
|
163
|
-
if (gen === spawnGen) inFlight = false
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
let out = ""
|
|
167
|
-
const timer = setTimeout(() => {
|
|
168
|
-
try {
|
|
169
|
-
child.kill()
|
|
170
|
-
} catch {
|
|
171
|
-
// already gone
|
|
172
|
-
}
|
|
173
|
-
}, GIT_TIMEOUT_MS)
|
|
174
|
-
child.stdout?.setEncoding("utf8")
|
|
175
|
-
child.stdout?.on("data", (chunk: string) => {
|
|
176
|
-
out += chunk
|
|
177
|
-
})
|
|
178
|
-
child.stderr?.resume()
|
|
179
|
-
const finish = (ok: boolean) => {
|
|
180
|
-
clearTimeout(timer)
|
|
181
|
-
if (gen !== spawnGen) return
|
|
182
|
-
inFlight = false
|
|
183
|
-
cacheRoot = root
|
|
184
|
-
cacheKey = key
|
|
185
|
-
if (ok) cacheMarks = parsePorcelainZ(out)
|
|
186
|
-
notify()
|
|
187
|
-
}
|
|
188
|
-
child.on("error", () => finish(false))
|
|
189
|
-
child.on("close", (code) => finish(code === 0))
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Last known porcelain letters. Never waits on git — a stale map is returned
|
|
194
|
-
* and a spawn is scheduled when the cache key is cold (2.5 s debounce on index noise).
|
|
195
|
-
*/
|
|
196
|
-
export function readGitMarksFor(
|
|
197
|
-
posixPaths: string[],
|
|
198
|
-
projectRoot?: string | null,
|
|
199
|
-
): {
|
|
200
|
-
root: string | null
|
|
201
|
-
marks: Map<string, GitLetter>
|
|
202
|
-
} {
|
|
203
|
-
const root = findGitRoot(projectRoot)
|
|
204
|
-
if (!root || posixPaths.length === 0) {
|
|
205
|
-
return { root, marks: new Map() }
|
|
206
|
-
}
|
|
207
|
-
const rels = relsFrom(posixPaths, root)
|
|
208
|
-
if (rels.length === 0) {
|
|
209
|
-
return { root, marks: new Map() }
|
|
210
|
-
}
|
|
211
|
-
const pathKey = rels.slice().sort().join("\0")
|
|
212
|
-
const stamp = gitStatusStamp(projectRoot)
|
|
213
|
-
const key = `${root}|${stamp}|${pathKey}`
|
|
214
|
-
cacheRoot = root
|
|
215
|
-
if (key === cacheKey) return { root, marks: cacheMarks }
|
|
216
|
-
if (inFlight && pendingKey === key) return { root, marks: cacheMarks }
|
|
217
|
-
|
|
218
|
-
const now = Date.now()
|
|
219
|
-
if (pathKey === lastPathKey && lastSpawn > 0 && now - lastSpawn < GIT_DEBOUNCE_MS) {
|
|
220
|
-
return { root, marks: cacheMarks }
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
pendingKey = key
|
|
224
|
-
lastPathKey = pathKey
|
|
225
|
-
lastSpawn = now
|
|
226
|
-
spawnGen += 1
|
|
227
|
-
inFlight = true
|
|
228
|
-
runGit(root, rels, key, spawnGen)
|
|
229
|
-
return { root, marks: cacheMarks }
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function gitLetterFor(posixPath: string, projectRoot?: string | null): GitLetter | null {
|
|
233
|
-
const { root, marks } = readGitMarksFor([posixPath], projectRoot)
|
|
234
|
-
if (!root || marks.size === 0) return null
|
|
235
|
-
const rel = relToGitRoot(posixPath, root)
|
|
236
|
-
return marks.get(rel) ?? marks.get(posixPath.replace(/\\/g, "/").toLowerCase()) ?? null
|
|
237
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Read-only git status for Files letters. No repo / no git → empty, never throws.
|
|
3
|
+
* Status is scoped to the listed paths — never the whole worktree.
|
|
4
|
+
* Spawn is async: the TUI keeps the last letters until git returns.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "node:child_process"
|
|
7
|
+
import fs from "node:fs"
|
|
8
|
+
import path from "node:path"
|
|
9
|
+
import { fileStamp } from "../pware.oc.core.paths.js"
|
|
10
|
+
|
|
11
|
+
/** Porcelain XY letters only. `?` is untracked. Do not invent extras here. */
|
|
12
|
+
export type GitLetter = "M" | "A" | "D" | "R" | "C" | "U" | "T" | "?"
|
|
13
|
+
|
|
14
|
+
const GIT_PATH_CAP = 40
|
|
15
|
+
const GIT_DEBOUNCE_MS = 2500
|
|
16
|
+
const GIT_TIMEOUT_MS = 1500
|
|
17
|
+
|
|
18
|
+
export function findGitRoot(start?: string | null): string | null {
|
|
19
|
+
if (!start) return null
|
|
20
|
+
let dir = path.resolve(start)
|
|
21
|
+
for (let i = 0; i < 16; i++) {
|
|
22
|
+
try {
|
|
23
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir
|
|
24
|
+
} catch {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
const parent = path.dirname(dir)
|
|
28
|
+
if (parent === dir) break
|
|
29
|
+
dir = parent
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function gitStatusStamp(projectRoot?: string | null): string {
|
|
35
|
+
const root = findGitRoot(projectRoot)
|
|
36
|
+
if (!root) return "0"
|
|
37
|
+
return [fileStamp(path.join(root, ".git")), fileStamp(path.join(root, ".git", "HEAD")), fileStamp(path.join(root, ".git", "index"))].join("|")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function porcelainLetter(xy: string): GitLetter | null {
|
|
41
|
+
if (xy === "??") return "?"
|
|
42
|
+
if (xy === "!!") return null
|
|
43
|
+
const x = xy[0] ?? " "
|
|
44
|
+
const y = xy[1] ?? " "
|
|
45
|
+
if (x === "U" || y === "U" || xy === "AA" || xy === "DD") return "U"
|
|
46
|
+
const rank = (c: string): GitLetter | null => {
|
|
47
|
+
if (c === "D") return "D"
|
|
48
|
+
if (c === "R") return "R"
|
|
49
|
+
if (c === "C") return "C"
|
|
50
|
+
if (c === "A") return "A"
|
|
51
|
+
if (c === "T") return "T"
|
|
52
|
+
if (c === "M") return "M"
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
return rank(x) ?? rank(y)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function xIsRename(x: string): boolean {
|
|
59
|
+
return x === "R" || x === "C"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Parse `git status --porcelain -z` stdout. Exported for tests. */
|
|
63
|
+
export function parsePorcelainZ(buf: string): Map<string, GitLetter> {
|
|
64
|
+
const out = new Map<string, GitLetter>()
|
|
65
|
+
let i = 0
|
|
66
|
+
while (i < buf.length) {
|
|
67
|
+
const xy = buf.slice(i, i + 2)
|
|
68
|
+
if (xy.length < 2) break
|
|
69
|
+
i += 2
|
|
70
|
+
if (buf[i] === " ") i += 1
|
|
71
|
+
const z = buf.indexOf("\0", i)
|
|
72
|
+
if (z < 0) break
|
|
73
|
+
const p1 = buf.slice(i, z).replace(/\\/g, "/")
|
|
74
|
+
i = z + 1
|
|
75
|
+
if (xIsRename(xy[0] ?? "")) {
|
|
76
|
+
const z2 = buf.indexOf("\0", i)
|
|
77
|
+
if (z2 >= 0) i = z2 + 1
|
|
78
|
+
}
|
|
79
|
+
const letter = porcelainLetter(xy)
|
|
80
|
+
if (letter && p1) out.set(p1.toLowerCase(), letter)
|
|
81
|
+
}
|
|
82
|
+
return out
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function relToGitRoot(posixPath: string, root: string): string {
|
|
86
|
+
const file = posixPath.replace(/\\/g, "/")
|
|
87
|
+
const base = root.replace(/\\/g, "/").replace(/\/+$/, "")
|
|
88
|
+
const lower = file.toLowerCase()
|
|
89
|
+
const rootLower = base.toLowerCase()
|
|
90
|
+
if (lower.startsWith(`${rootLower}/`)) return lower.slice(rootLower.length + 1)
|
|
91
|
+
return lower.replace(/^\.\//, "")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Pathspecs for `git status`: repo paths only, deduped, capped. Exported for tests. */
|
|
95
|
+
export function relsFrom(posixPaths: string[], root: string): string[] {
|
|
96
|
+
const base = root.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase()
|
|
97
|
+
const seen = new Set<string>()
|
|
98
|
+
const out: string[] = []
|
|
99
|
+
for (const p of posixPaths) {
|
|
100
|
+
const file = p.replace(/\\/g, "/")
|
|
101
|
+
// git aborts the whole status with exit 128 on a single outside-repo
|
|
102
|
+
// pathspec — sessions touch temp/config files outside the tree all the time.
|
|
103
|
+
if (!file.toLowerCase().startsWith(`${base}/`)) continue
|
|
104
|
+
const rel = relToGitRoot(p, root)
|
|
105
|
+
if (!rel || seen.has(rel)) continue
|
|
106
|
+
seen.add(rel)
|
|
107
|
+
out.push(rel)
|
|
108
|
+
if (out.length >= GIT_PATH_CAP) break
|
|
109
|
+
}
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let cacheKey = ""
|
|
114
|
+
let cacheMarks = new Map<string, GitLetter>()
|
|
115
|
+
let cacheRoot: string | null = null
|
|
116
|
+
let lastPathKey = ""
|
|
117
|
+
let lastSpawn = 0
|
|
118
|
+
let pendingKey = ""
|
|
119
|
+
let spawnGen = 0
|
|
120
|
+
let inFlight = false
|
|
121
|
+
|
|
122
|
+
const listeners = new Set<() => void>()
|
|
123
|
+
|
|
124
|
+
function notify(): void {
|
|
125
|
+
for (const fn of listeners) {
|
|
126
|
+
try {
|
|
127
|
+
fn()
|
|
128
|
+
} catch {
|
|
129
|
+
// sidebar teardown
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Re-render when an async git status lands. Returns unsubscribe. */
|
|
135
|
+
export function onGitMarksChange(fn: () => void): () => void {
|
|
136
|
+
listeners.add(fn)
|
|
137
|
+
return () => {
|
|
138
|
+
listeners.delete(fn)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Drop cached letters and in-flight spawn accounting (tests). */
|
|
143
|
+
export function resetGitCache(): void {
|
|
144
|
+
spawnGen += 1
|
|
145
|
+
inFlight = false
|
|
146
|
+
cacheKey = ""
|
|
147
|
+
pendingKey = ""
|
|
148
|
+
lastPathKey = ""
|
|
149
|
+
lastSpawn = 0
|
|
150
|
+
cacheRoot = null
|
|
151
|
+
cacheMarks = new Map()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function runGit(root: string, rels: string[], key: string, gen: number): void {
|
|
155
|
+
let child: ReturnType<typeof spawn>
|
|
156
|
+
try {
|
|
157
|
+
child = spawn(
|
|
158
|
+
"git",
|
|
159
|
+
["-c", "core.quotepath=false", "status", "--porcelain", "-z", "--", ...rels],
|
|
160
|
+
{ cwd: root, windowsHide: true },
|
|
161
|
+
)
|
|
162
|
+
} catch {
|
|
163
|
+
if (gen === spawnGen) inFlight = false
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
let out = ""
|
|
167
|
+
const timer = setTimeout(() => {
|
|
168
|
+
try {
|
|
169
|
+
child.kill()
|
|
170
|
+
} catch {
|
|
171
|
+
// already gone
|
|
172
|
+
}
|
|
173
|
+
}, GIT_TIMEOUT_MS)
|
|
174
|
+
child.stdout?.setEncoding("utf8")
|
|
175
|
+
child.stdout?.on("data", (chunk: string) => {
|
|
176
|
+
out += chunk
|
|
177
|
+
})
|
|
178
|
+
child.stderr?.resume()
|
|
179
|
+
const finish = (ok: boolean) => {
|
|
180
|
+
clearTimeout(timer)
|
|
181
|
+
if (gen !== spawnGen) return
|
|
182
|
+
inFlight = false
|
|
183
|
+
cacheRoot = root
|
|
184
|
+
cacheKey = key
|
|
185
|
+
if (ok) cacheMarks = parsePorcelainZ(out)
|
|
186
|
+
notify()
|
|
187
|
+
}
|
|
188
|
+
child.on("error", () => finish(false))
|
|
189
|
+
child.on("close", (code) => finish(code === 0))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Last known porcelain letters. Never waits on git — a stale map is returned
|
|
194
|
+
* and a spawn is scheduled when the cache key is cold (2.5 s debounce on index noise).
|
|
195
|
+
*/
|
|
196
|
+
export function readGitMarksFor(
|
|
197
|
+
posixPaths: string[],
|
|
198
|
+
projectRoot?: string | null,
|
|
199
|
+
): {
|
|
200
|
+
root: string | null
|
|
201
|
+
marks: Map<string, GitLetter>
|
|
202
|
+
} {
|
|
203
|
+
const root = findGitRoot(projectRoot)
|
|
204
|
+
if (!root || posixPaths.length === 0) {
|
|
205
|
+
return { root, marks: new Map() }
|
|
206
|
+
}
|
|
207
|
+
const rels = relsFrom(posixPaths, root)
|
|
208
|
+
if (rels.length === 0) {
|
|
209
|
+
return { root, marks: new Map() }
|
|
210
|
+
}
|
|
211
|
+
const pathKey = rels.slice().sort().join("\0")
|
|
212
|
+
const stamp = gitStatusStamp(projectRoot)
|
|
213
|
+
const key = `${root}|${stamp}|${pathKey}`
|
|
214
|
+
cacheRoot = root
|
|
215
|
+
if (key === cacheKey) return { root, marks: cacheMarks }
|
|
216
|
+
if (inFlight && pendingKey === key) return { root, marks: cacheMarks }
|
|
217
|
+
|
|
218
|
+
const now = Date.now()
|
|
219
|
+
if (pathKey === lastPathKey && lastSpawn > 0 && now - lastSpawn < GIT_DEBOUNCE_MS) {
|
|
220
|
+
return { root, marks: cacheMarks }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
pendingKey = key
|
|
224
|
+
lastPathKey = pathKey
|
|
225
|
+
lastSpawn = now
|
|
226
|
+
spawnGen += 1
|
|
227
|
+
inFlight = true
|
|
228
|
+
runGit(root, rels, key, spawnGen)
|
|
229
|
+
return { root, marks: cacheMarks }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function gitLetterFor(posixPath: string, projectRoot?: string | null): GitLetter | null {
|
|
233
|
+
const { root, marks } = readGitMarksFor([posixPath], projectRoot)
|
|
234
|
+
if (!root || marks.size === 0) return null
|
|
235
|
+
const rel = relToGitRoot(posixPath, root)
|
|
236
|
+
return marks.get(rel) ?? marks.get(posixPath.replace(/\\/g, "/").toLowerCase()) ?? null
|
|
237
|
+
}
|