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.
Files changed (82) hide show
  1. package/.claude/skills/k0-changelog/SKILL.md +2 -2
  2. package/.claude/skills/k0-discuss/SKILL.md +105 -0
  3. package/.claude/skills/k0-discuss/references/rounds.md +280 -0
  4. package/.claude/skills/k0-epic/SKILL.md +196 -0
  5. package/.claude/skills/k0-links/.claude-plugin/plugin.json +6 -0
  6. package/.claude/skills/k0-links/hooks/hooks.json +1 -0
  7. package/.claude/skills/k0-links/hooks/links.js +128 -0
  8. package/.claude/skills/k0-links/hooks/register.js +204 -0
  9. package/.claude/skills/k0-next/SKILL.md +82 -0
  10. package/.claude/skills/k0-order/SKILL.md +123 -0
  11. package/.claude/skills/k0-plan/SKILL.md +156 -0
  12. package/.claude/skills/k0-split/SKILL.md +169 -0
  13. package/.claude/skills/k0-story/SKILL.md +108 -0
  14. package/.claude/skills/k0-ultracode/SKILL.md +251 -0
  15. package/.claude/skills/k0-ultracode/references/agent-brief.md +71 -0
  16. package/.claude/skills/k0-ultracode/references/modes.md +49 -0
  17. package/.claude/skills/k0-verify/SKILL.md +219 -0
  18. package/.claude/skills/k0-whatsnew/SKILL.md +95 -0
  19. package/.claude/skills/k0-work/SKILL.md +212 -0
  20. package/CHANGELOG.md +426 -1
  21. package/README.md +1021 -119
  22. package/bin/k0-board.js +81 -11
  23. package/menubar/K0MenuBar.swift +35 -5
  24. package/package.json +13 -1
  25. package/platform/contract.js +37 -6
  26. package/platform/darwin/index.js +1 -0
  27. package/platform/darwin/shell.js +4 -1
  28. package/platform/darwin/terminal.js +230 -56
  29. package/platform/index.js +3 -1
  30. package/platform/linux/index.js +14 -0
  31. package/platform/linux/shell.js +10 -1
  32. package/platform/linux/tray.py +10 -10
  33. package/platform/shared/command.js +19 -3
  34. package/platform/shared/tmux.js +32 -14
  35. package/platform/win32/index.js +6 -2
  36. package/platform/win32/shell.js +4 -1
  37. package/platform/win32/terminal.js +24 -8
  38. package/platform/win32/tray.ps1 +13 -13
  39. package/server/backlog.js +618 -0
  40. package/server/changelog.js +13 -13
  41. package/server/db.js +1800 -146
  42. package/server/files.js +156 -26
  43. package/server/git.js +53 -14
  44. package/server/idle.js +15 -13
  45. package/server/index.js +1286 -189
  46. package/server/launcher.js +122 -14
  47. package/server/machine.js +10 -1
  48. package/server/mirror.js +1048 -0
  49. package/server/prices.js +59 -0
  50. package/server/projects.js +57 -12
  51. package/server/servers.js +22 -1
  52. package/server/sessions.js +5 -5
  53. package/server/settings.js +25 -2
  54. package/server/update.js +324 -0
  55. package/server/usage.js +642 -0
  56. package/server/watcher.js +67 -13
  57. package/server/worktree.js +600 -0
  58. package/server/writer.js +98 -26
  59. package/web/awake.js +62 -0
  60. package/web/base.css +4 -0
  61. package/web/board.css +404 -16
  62. package/web/board.js +1182 -152
  63. package/web/changelog.js +3 -3
  64. package/web/conf.js +118 -0
  65. package/web/files.css +206 -1
  66. package/web/files.html +10 -0
  67. package/web/files.js +533 -75
  68. package/web/fuzzy.js +73 -21
  69. package/web/index.html +109 -12
  70. package/web/json.js +145 -0
  71. package/web/list.css +536 -0
  72. package/web/list.js +1015 -0
  73. package/web/mentions.js +27 -9
  74. package/web/recency.js +10 -10
  75. package/web/units.js +1 -1
  76. package/web/usage.css +169 -0
  77. package/web/usage.html +78 -0
  78. package/web/usage.js +447 -0
  79. package/web/view.js +22 -3
  80. package/web/whatsnew.css +211 -0
  81. package/web/whatsnew.html +49 -0
  82. package/web/whatsnew.js +339 -0
@@ -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 card's terminal disappear
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 card's name in the window's
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
- export async function focus(handle) {
187
- if (!handle) return { ok: false, error: 'This card has no terminal of its own' }
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 86% margin as everywhere else is applied to it.
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 * 0.86)
207
- const h = Math.round(sh * 0.86)
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
- * Font size belongs to the emulator, and every emulator spells it differently — some only in
223
- * a config file that is read at startup. There is no honest generic answer, so k0 says it
224
- * cannot and driving mode changes only the board's own text.
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 async function setFont() {
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'
@@ -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 card.',
37
+ 'again from the story.',
37
38
  'terminal.title':
38
- 'Windows Terminal owns its tab titles, so renaming a card does not rename a window that ' +
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.',
@@ -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 card renamed after its session started keeps the old name in the title bar
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 card has no terminal window of its own' }
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 * 0.86); $h = [int]($area.Height * 0.86)
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
- /** Font size lives in the terminal's own profile, which k0 will not rewrite. */
109
- export async function setFont() {
110
- return { touched: 0 }
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'
@@ -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 card 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/card/$($script:LastNotified)/focus" @{} | Out-Null
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 ($card in $Waiting) {
104
- $label = "$($Labels[$card.status]) - $($card.title)"
105
- $id = $card.id
106
- Add-K0Item $label ([System.EventHandler]{ Invoke-K0 "/api/card/$id/focus" @{} | Out-Null }.GetNewClosure()) | Out-Null
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 cards that have just started waiting, not about every one on every pass.
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 ($card in $Waiting) { $now["$($card.id):$($card.status)"] = $true }
132
+ foreach ($story in $Waiting) { $now["$($story.id):$($story.status)"] = $true }
133
133
  if (-not $script:FirstPass) {
134
- foreach ($card in $Waiting) {
135
- $key = "$($card.id):$($card.status)"
134
+ foreach ($story in $Waiting) {
135
+ $key = "$($story.id):$($story.status)"
136
136
  if (-not $script:Seen.ContainsKey($key)) {
137
- $script:LastNotified = $card.id
138
- $notify.BalloonTipTitle = $Labels[$card.status]
139
- $notify.BalloonTipText = $card.title
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
  }