k0-board 0.3.0 → 0.5.0
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/.claude/skills/k0-changelog/SKILL.md +2 -2
- package/.claude/skills/k0-discuss/SKILL.md +105 -0
- package/.claude/skills/k0-discuss/references/rounds.md +280 -0
- package/.claude/skills/k0-epic/SKILL.md +196 -0
- package/.claude/skills/k0-links/.claude-plugin/plugin.json +6 -0
- package/.claude/skills/k0-links/hooks/hooks.json +1 -0
- package/.claude/skills/k0-links/hooks/links.js +128 -0
- package/.claude/skills/k0-links/hooks/register.js +204 -0
- package/.claude/skills/k0-next/SKILL.md +82 -0
- package/.claude/skills/k0-order/SKILL.md +123 -0
- package/.claude/skills/k0-plan/SKILL.md +156 -0
- package/.claude/skills/k0-split/SKILL.md +169 -0
- package/.claude/skills/k0-story/SKILL.md +108 -0
- package/.claude/skills/k0-ultracode/SKILL.md +251 -0
- package/.claude/skills/k0-ultracode/references/agent-brief.md +71 -0
- package/.claude/skills/k0-ultracode/references/modes.md +49 -0
- package/.claude/skills/k0-verify/SKILL.md +219 -0
- package/.claude/skills/k0-whatsnew/SKILL.md +95 -0
- package/.claude/skills/k0-work/SKILL.md +212 -0
- package/CHANGELOG.md +426 -1
- package/README.md +1021 -119
- package/bin/k0-board.js +81 -11
- package/menubar/K0MenuBar.swift +35 -5
- package/package.json +13 -1
- package/platform/contract.js +37 -6
- package/platform/darwin/index.js +1 -0
- package/platform/darwin/shell.js +4 -1
- package/platform/darwin/terminal.js +230 -56
- package/platform/index.js +3 -1
- package/platform/linux/index.js +14 -0
- package/platform/linux/shell.js +10 -1
- package/platform/linux/tray.py +10 -10
- package/platform/shared/command.js +19 -3
- package/platform/shared/tmux.js +32 -14
- package/platform/win32/index.js +6 -2
- package/platform/win32/shell.js +4 -1
- package/platform/win32/terminal.js +24 -8
- package/platform/win32/tray.ps1 +13 -13
- package/server/backlog.js +618 -0
- package/server/changelog.js +13 -13
- package/server/db.js +1800 -146
- package/server/files.js +156 -26
- package/server/git.js +53 -14
- package/server/idle.js +15 -13
- package/server/index.js +1286 -189
- package/server/launcher.js +122 -14
- package/server/machine.js +10 -1
- package/server/mirror.js +1048 -0
- package/server/prices.js +59 -0
- package/server/projects.js +57 -12
- package/server/servers.js +22 -1
- package/server/sessions.js +5 -5
- package/server/settings.js +25 -2
- package/server/update.js +324 -0
- package/server/usage.js +642 -0
- package/server/watcher.js +67 -13
- package/server/worktree.js +600 -0
- package/server/writer.js +98 -26
- package/web/awake.js +62 -0
- package/web/base.css +4 -0
- package/web/board.css +404 -16
- package/web/board.js +1182 -152
- package/web/changelog.js +3 -3
- package/web/conf.js +118 -0
- package/web/files.css +206 -1
- package/web/files.html +10 -0
- package/web/files.js +533 -75
- package/web/fuzzy.js +73 -21
- package/web/index.html +109 -12
- package/web/json.js +145 -0
- package/web/list.css +536 -0
- package/web/list.js +1015 -0
- package/web/mentions.js +27 -9
- package/web/recency.js +10 -10
- package/web/units.js +1 -1
- package/web/usage.css +169 -0
- package/web/usage.html +78 -0
- package/web/usage.js +447 -0
- package/web/view.js +22 -3
- package/web/whatsnew.css +211 -0
- package/web/whatsnew.html +49 -0
- package/web/whatsnew.js +339 -0
package/platform/shared/tmux.js
CHANGED
|
@@ -36,6 +36,9 @@ const EMULATORS = [
|
|
|
36
36
|
{ bin: 'xterm', args: (cmd) => ['-e', ...cmd] },
|
|
37
37
|
]
|
|
38
38
|
|
|
39
|
+
/** How much of the work area a window takes unless the mode asks for more. */
|
|
40
|
+
const COVERAGE = 0.86
|
|
41
|
+
|
|
39
42
|
/**
|
|
40
43
|
* Which emulator to open. `K0_TERMINAL` wins, because no list of ours will ever cover every
|
|
41
44
|
* terminal anybody uses, and being wrong about it should not mean being stuck with it.
|
|
@@ -83,10 +86,10 @@ export async function open({ command, title }) {
|
|
|
83
86
|
const session = sessionName()
|
|
84
87
|
|
|
85
88
|
// The session runs the command directly rather than a shell that then runs it: when Claude
|
|
86
|
-
// Code exits, the session ends, which is what makes a finished
|
|
89
|
+
// Code exits, the session ends, which is what makes a finished story's terminal disappear
|
|
87
90
|
// instead of leaving an idle shell behind.
|
|
88
91
|
await run(tmux, ['new-session', '-d', '-s', session, '-n', title || session, command])
|
|
89
|
-
// Off by default, and worth turning on: it is what puts the
|
|
92
|
+
// Off by default, and worth turning on: it is what puts the story's name in the window's
|
|
90
93
|
// title bar rather than "tmux".
|
|
91
94
|
await runQuiet(tmux, ['set-option', '-t', session, 'set-titles', 'on'])
|
|
92
95
|
await runQuiet(tmux, ['set-option', '-t', session, 'set-titles-string', title || session])
|
|
@@ -183,8 +186,12 @@ async function windowId(handle) {
|
|
|
183
186
|
return String(out).trim().split('\n').filter(Boolean).pop() || null
|
|
184
187
|
}
|
|
185
188
|
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
/**
|
|
190
|
+
* `title` is accepted and unused: here the handle IS the name tmux put on the window, so the
|
|
191
|
+
* lookup below is already the search by name that macOS has to fall back to.
|
|
192
|
+
*/
|
|
193
|
+
export async function focus(handle, title) {
|
|
194
|
+
if (!handle) return { ok: false, error: 'This story has no terminal of its own' }
|
|
188
195
|
if (!canPlaceWindows()) return { ok: false, error: 'k0 cannot raise windows on this desktop' }
|
|
189
196
|
const id = await windowId(handle)
|
|
190
197
|
if (!id) return { ok: false, error: 'That window is gone' }
|
|
@@ -195,16 +202,17 @@ export async function focus(handle) {
|
|
|
195
202
|
/**
|
|
196
203
|
* Geometry, best effort. There is no NSScreen here to ask for the usable area net of panels
|
|
197
204
|
* and docks, so the work area comes from the window manager's own `_NET_WORKAREA` by way of
|
|
198
|
-
* xdotool, and the same
|
|
205
|
+
* xdotool, and the same share of it as everywhere else is applied — all of it when the mode asks
|
|
206
|
+
* for windows readable from across the room.
|
|
199
207
|
*/
|
|
200
|
-
export async function relayout(handles) {
|
|
208
|
+
export async function relayout(handles, { coverage = COVERAGE } = {}) {
|
|
201
209
|
if (!canPlaceWindows()) return { touched: 0 }
|
|
202
210
|
const xdotool = XDOTOOL()
|
|
203
211
|
const geom = await runQuiet(xdotool, ['getdisplaygeometry'])
|
|
204
212
|
const [sw, sh] = String(geom).trim().split(/\s+/).map(Number)
|
|
205
213
|
if (!sw || !sh) return { touched: 0 }
|
|
206
|
-
const w = Math.round(sw *
|
|
207
|
-
const h = Math.round(sh *
|
|
214
|
+
const w = Math.round(sw * coverage)
|
|
215
|
+
const h = Math.round(sh * coverage)
|
|
208
216
|
const x = Math.round((sw - w) / 2)
|
|
209
217
|
const y = Math.round((sh - h) / 2)
|
|
210
218
|
let touched = 0
|
|
@@ -219,24 +227,34 @@ export async function relayout(handles) {
|
|
|
219
227
|
}
|
|
220
228
|
|
|
221
229
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
230
|
+
* What the mode asks for, as far as this platform can give it: the window takes its share of the
|
|
231
|
+
* screen. Font size belongs to the emulator, and every emulator spells it differently — some only
|
|
232
|
+
* in a config file read at startup. There is no honest generic answer, so k0 says it cannot: here
|
|
233
|
+
* driving mode makes the windows bigger and leaves the text to the emulator.
|
|
225
234
|
*/
|
|
226
|
-
export
|
|
227
|
-
return { touched: 0 }
|
|
228
|
-
}
|
|
235
|
+
export const applyMode = (handles, { coverage } = {}) => relayout(handles, { coverage })
|
|
229
236
|
|
|
230
237
|
export async function defaultFontSize() {
|
|
231
238
|
return 12
|
|
232
239
|
}
|
|
233
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Not yet. `send-keys` would probably do it, but it has not been seen to: whether Claude Code
|
|
243
|
+
* takes a slash command that arrives that way, or reads it as a message the way it reads the
|
|
244
|
+
* block `type` writes on macOS, is exactly the kind of thing this file does not guess at. Until
|
|
245
|
+
* somebody checks on a Linux machine, a live session keeps its name until it ends.
|
|
246
|
+
*/
|
|
247
|
+
export async function command() {
|
|
248
|
+
return { sent: false, why: 'Running a command inside a live session is not available with tmux yet' }
|
|
249
|
+
}
|
|
250
|
+
|
|
234
251
|
export const capabilities = () => ({
|
|
235
252
|
windows: canPlaceWindows(),
|
|
236
253
|
font: false,
|
|
237
254
|
readScreen: !!TMUX(),
|
|
238
255
|
pasteWithoutSending: !!TMUX(),
|
|
239
256
|
title: !!TMUX(),
|
|
257
|
+
commands: false,
|
|
240
258
|
})
|
|
241
259
|
|
|
242
260
|
export { posixCommand as buildCommand } from './command.js'
|
package/platform/win32/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export const capabilities = {
|
|
|
24
24
|
service: service.capabilities(),
|
|
25
25
|
tray: hasPowerShell(),
|
|
26
26
|
revealInFileManager: true,
|
|
27
|
+
openInFileManager: true,
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export const notes = {
|
|
@@ -33,10 +34,13 @@ export const notes = {
|
|
|
33
34
|
'Windows gives no way to read what is printed inside another process’s console, so k0 ' +
|
|
34
35
|
'cannot tell exactly when Claude Code has finished starting: it waits a fixed moment ' +
|
|
35
36
|
'instead. On a slow machine the prompt can arrive early — press Escape, then paste it ' +
|
|
36
|
-
'again from the
|
|
37
|
+
'again from the story.',
|
|
37
38
|
'terminal.title':
|
|
38
|
-
'Windows Terminal owns its tab titles, so renaming a
|
|
39
|
+
'Windows Terminal owns its tab titles, so renaming a story does not rename a window that ' +
|
|
39
40
|
'is already open. The new name shows up next time the session starts.',
|
|
41
|
+
'terminal.commands':
|
|
42
|
+
'Renaming a story renames its Claude Code session only once the session has ended: k0 ' +
|
|
43
|
+
'cannot read what is on a terminal’s screen here, so it will not type /rename into one blind.',
|
|
40
44
|
'terminal.font':
|
|
41
45
|
'Font size lives in your terminal’s own profile, which k0 will not rewrite, so driving ' +
|
|
42
46
|
'mode enlarges the board but not the terminals.',
|
package/platform/win32/shell.js
CHANGED
|
@@ -8,6 +8,9 @@ const EXPLORER = () => which('explorer') || 'explorer.exe'
|
|
|
8
8
|
/** `explorer /select,` opens the folder with the file already highlighted. */
|
|
9
9
|
export const revealInFileManager = (abs) => runQuiet(EXPLORER(), [`/select,${path.normalize(abs)}`])
|
|
10
10
|
|
|
11
|
+
/** The same command without `/select,` opens the folder itself. */
|
|
12
|
+
export const openInFileManager = (abs) => runQuiet(EXPLORER(), [path.normalize(abs)])
|
|
13
|
+
|
|
11
14
|
export const openBrowser = (url) => runQuiet('cmd', ['/c', 'start', '', url])
|
|
12
15
|
|
|
13
16
|
const programFiles = [
|
|
@@ -61,4 +64,4 @@ export function findClaude() {
|
|
|
61
64
|
return claudeBin
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
export const capabilities = () => ({ revealInFileManager: true })
|
|
67
|
+
export const capabilities = () => ({ revealInFileManager: true, openInFileManager: true })
|
|
@@ -26,6 +26,9 @@ import { ps, psJson, psq } from './powershell.js'
|
|
|
26
26
|
const WT = () => which('wt')
|
|
27
27
|
const POWERSHELL_BIN = () => which('pwsh') || which('powershell')
|
|
28
28
|
|
|
29
|
+
/** How much of the work area a window takes unless the mode asks for more. */
|
|
30
|
+
const COVERAGE = 0.86
|
|
31
|
+
|
|
29
32
|
/** The user32 declarations every window operation here needs, added once per PowerShell run. */
|
|
30
33
|
const USER32 = `
|
|
31
34
|
Add-Type -Namespace K0 -Name Win -MemberDefinition @'
|
|
@@ -66,7 +69,7 @@ export async function open({ command, title }) {
|
|
|
66
69
|
|
|
67
70
|
/**
|
|
68
71
|
* Windows Terminal owns its tab titles and does not take instructions about them from
|
|
69
|
-
* outside, so a
|
|
72
|
+
* outside, so a story renamed after its session started keeps the old name in the title bar
|
|
70
73
|
* until the session is restarted. Reported as unavailable rather than failing silently.
|
|
71
74
|
*/
|
|
72
75
|
export async function setTitle() {
|
|
@@ -74,7 +77,7 @@ export async function setTitle() {
|
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
export async function focus(handle) {
|
|
77
|
-
if (!handle) return { ok: false, error: 'This
|
|
80
|
+
if (!handle) return { ok: false, error: 'This story has no terminal window of its own' }
|
|
78
81
|
const found = await psJson(
|
|
79
82
|
`${USER32}
|
|
80
83
|
$p = ${findWindow(handle)}
|
|
@@ -86,14 +89,14 @@ export async function focus(handle) {
|
|
|
86
89
|
return found?.ok ? { ok: true } : { ok: false, error: 'That window is gone' }
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
export async function relayout(handles) {
|
|
92
|
+
export async function relayout(handles, { coverage = COVERAGE } = {}) {
|
|
90
93
|
let touched = 0
|
|
91
94
|
for (const handle of handles || []) {
|
|
92
95
|
const ok = await psJson(
|
|
93
96
|
`${USER32}
|
|
94
97
|
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue
|
|
95
98
|
$area = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
|
|
96
|
-
$w = [int]($area.Width *
|
|
99
|
+
$w = [int]($area.Width * ${coverage}); $h = [int]($area.Height * ${coverage})
|
|
97
100
|
$x = $area.X + [int](($area.Width - $w) / 2); $y = $area.Y + [int](($area.Height - $h) / 2)
|
|
98
101
|
$p = ${findWindow(handle)}
|
|
99
102
|
if ($p) { [K0.Win]::MoveWindow($p.MainWindowHandle, $x, $y, $w, $h, $true) | Out-Null; @{ ok = $true } }
|
|
@@ -105,10 +108,13 @@ export async function relayout(handles) {
|
|
|
105
108
|
return { touched }
|
|
106
109
|
}
|
|
107
110
|
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
/**
|
|
112
|
+
* What the mode asks for, as far as this platform can give it: the window takes its share of
|
|
113
|
+
* the screen. The font size lives in the terminal's own profile, which k0 will not rewrite —
|
|
114
|
+
* so here driving mode makes the windows bigger and leaves the text alone, which is what the
|
|
115
|
+
* `terminal.font` capability has always said.
|
|
116
|
+
*/
|
|
117
|
+
export const applyMode = (handles, { coverage } = {}) => relayout(handles, { coverage })
|
|
112
118
|
|
|
113
119
|
export async function defaultFontSize() {
|
|
114
120
|
return 12
|
|
@@ -198,12 +204,22 @@ export async function close({ handle, pid }) {
|
|
|
198
204
|
return { closed: !!res?.ok }
|
|
199
205
|
}
|
|
200
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Not here: a command is only worth typing into a live session once the screen says the input
|
|
209
|
+
* box is free and, afterwards, that the line came out right — and Windows gives no way to read
|
|
210
|
+
* the screen (see the top of this file). A live session keeps its name until it ends.
|
|
211
|
+
*/
|
|
212
|
+
export async function command() {
|
|
213
|
+
return { sent: false, why: 'k0 cannot read a terminal on Windows, so it will not type into one blind' }
|
|
214
|
+
}
|
|
215
|
+
|
|
201
216
|
export const capabilities = () => ({
|
|
202
217
|
windows: !!POWERSHELL_BIN(),
|
|
203
218
|
font: false,
|
|
204
219
|
readScreen: false,
|
|
205
220
|
pasteWithoutSending: !!POWERSHELL_BIN(),
|
|
206
221
|
title: false,
|
|
222
|
+
commands: false,
|
|
207
223
|
})
|
|
208
224
|
|
|
209
225
|
export { powershellCommand as buildCommand } from '../shared/command.js'
|
package/platform/win32/tray.ps1
CHANGED
|
@@ -75,12 +75,12 @@ $menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
|
75
75
|
$notify.ContextMenuStrip = $menu
|
|
76
76
|
|
|
77
77
|
# Clicking the balloon brings that session's terminal up front, which is the whole point of being
|
|
78
|
-
# told. Windows shows one balloon at a time and replaces it rather than queueing, so the
|
|
78
|
+
# told. Windows shows one balloon at a time and replaces it rather than queueing, so the story the
|
|
79
79
|
# click belongs to is always the last one announced — there is nothing else it could be.
|
|
80
80
|
$script:LastNotified = $null
|
|
81
81
|
$notify.Add_BalloonTipClicked({
|
|
82
82
|
if ($null -ne $script:LastNotified) {
|
|
83
|
-
Invoke-K0 "/api/
|
|
83
|
+
Invoke-K0 "/api/story/$($script:LastNotified)/focus" @{} | Out-Null
|
|
84
84
|
}
|
|
85
85
|
}) | Out-Null
|
|
86
86
|
|
|
@@ -100,10 +100,10 @@ function Update-K0Menu($Waiting, [bool]$Down) {
|
|
|
100
100
|
} elseif (-not $Waiting -or $Waiting.Count -eq 0) {
|
|
101
101
|
Add-K0Item 'Nothing waiting for you' $null $false | Out-Null
|
|
102
102
|
} else {
|
|
103
|
-
foreach ($
|
|
104
|
-
$label = "$($Labels[$
|
|
105
|
-
$id = $
|
|
106
|
-
Add-K0Item $label ([System.EventHandler]{ Invoke-K0 "/api/
|
|
103
|
+
foreach ($story in $Waiting) {
|
|
104
|
+
$label = "$($Labels[$story.status]) - $($story.title)"
|
|
105
|
+
$id = $story.id
|
|
106
|
+
Add-K0Item $label ([System.EventHandler]{ Invoke-K0 "/api/story/$id/focus" @{} | Out-Null }.GetNewClosure()) | Out-Null
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -126,17 +126,17 @@ function Update-K0Menu($Waiting, [bool]$Down) {
|
|
|
126
126
|
}) | Out-Null
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
# Notify about
|
|
129
|
+
# Notify about stories that have just started waiting, not about every one on every pass.
|
|
130
130
|
function Show-K0Notifications($Waiting) {
|
|
131
131
|
$now = @{}
|
|
132
|
-
foreach ($
|
|
132
|
+
foreach ($story in $Waiting) { $now["$($story.id):$($story.status)"] = $true }
|
|
133
133
|
if (-not $script:FirstPass) {
|
|
134
|
-
foreach ($
|
|
135
|
-
$key = "$($
|
|
134
|
+
foreach ($story in $Waiting) {
|
|
135
|
+
$key = "$($story.id):$($story.status)"
|
|
136
136
|
if (-not $script:Seen.ContainsKey($key)) {
|
|
137
|
-
$script:LastNotified = $
|
|
138
|
-
$notify.BalloonTipTitle = $Labels[$
|
|
139
|
-
$notify.BalloonTipText = $
|
|
137
|
+
$script:LastNotified = $story.id
|
|
138
|
+
$notify.BalloonTipTitle = $Labels[$story.status]
|
|
139
|
+
$notify.BalloonTipText = $story.title
|
|
140
140
|
$notify.ShowBalloonTip(4000)
|
|
141
141
|
}
|
|
142
142
|
}
|