thinkpool-pair 0.7.338 → 0.7.339

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/bridge.mjs CHANGED
@@ -58,6 +58,7 @@ import { startStructuredSession } from './runtime-session.mjs'
58
58
  import { fallbackTerminalName } from './terminal-name.mjs'
59
59
  import { defaultStructuredMode, normalizeStructuredEffort, shouldDeferStructuredRuntime, structuredModeForSlice, structuredModeLocked, structuredModesForLane, structuredRuntimeForCommand, structuredRuntimeMetadata, structuredRuntimeSupportsMode } from './runtime-registry.mjs'
60
60
  import { commandCatalogForRuntime, commandHelpLine, reconcileCommandCatalog } from './command-catalog.mjs'
61
+ import { gitDiffReport } from './git-diff-report.mjs'
61
62
  import { probeHermesRuntime } from './hermes-probe.mjs'
62
63
  import { hermesRequiredMcpTools, hermesRoleFor } from './hermes-policy.mjs'
63
64
  import { canonicalRoomFilePath, waitForNativeImages } from './codex-images.mjs'
@@ -4365,8 +4366,7 @@ channel
4365
4366
  if (/^\/diff\s*$/.test(text)) {
4366
4367
  try {
4367
4368
  const cwd = s.cwd || process.cwd()
4368
- const summary = execFileSync('git', ['-C', cwd, 'status', '--short'], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] }).trim()
4369
- ctlLine(summary ? `Working tree changes\n${summary.slice(0, 1600)}` : 'Working tree clean')
4369
+ ctlLine(gitDiffReport({ cwd }))
4370
4370
  } catch { ctlLine('Working-tree diff unavailable outside a readable Git checkout') }
4371
4371
  return
4372
4372
  }
@@ -19,7 +19,7 @@ export const CODE_ROOM_COMMANDS = Object.freeze([
19
19
  command('/status', 'runtime, model, permissions, and busy state', 'control'),
20
20
  command('/usage', 'session usage and provider limits', 'control'),
21
21
  command('/context', 'current context-window usage', 'control'),
22
- command('/diff', 'working-tree change summary', 'control'),
22
+ command('/diff', 'Git changes and branch status', 'control'),
23
23
  command('/compact', 'compact context', 'runtime'),
24
24
  command('/clear', 'clear context · confirms', 'clear'),
25
25
  command('/model', 'pick model — opens selector', 'model'),
@@ -0,0 +1,118 @@
1
+ import { execFileSync } from 'node:child_process'
2
+
3
+ const DEFAULT_MAX_FILES = 24
4
+ const DEFAULT_MAX_COMMITS = 5
5
+ const DEFAULT_MAX_CHARS = 1800
6
+
7
+ const plural = (count, word) => `${count} ${word}${count === 1 ? '' : 's'}`
8
+
9
+ function defaultRunGit(args, cwd) {
10
+ return execFileSync('git', args, {
11
+ cwd,
12
+ encoding: 'utf8',
13
+ timeout: 3000,
14
+ stdio: ['ignore', 'pipe', 'pipe'],
15
+ })
16
+ }
17
+
18
+ function cleanLine(value, max = 240) {
19
+ return String(value || '')
20
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '')
21
+ .replace(/\s+/g, ' ')
22
+ .trim()
23
+ .slice(0, max)
24
+ }
25
+
26
+ function gitText(runGit, cwd, args) {
27
+ return String(runGit(args, cwd) || '').trim()
28
+ }
29
+
30
+ function tryGit(runGit, cwd, args) {
31
+ try { return gitText(runGit, cwd, args) } catch { return '' }
32
+ }
33
+
34
+ function resolveComparisonRef(runGit, cwd) {
35
+ const remoteHead = tryGit(runGit, cwd, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'])
36
+ if (remoteHead && tryGit(runGit, cwd, ['rev-parse', '--verify', '--quiet', remoteHead])) return remoteHead
37
+ for (const candidate of ['origin/main', 'origin/master', 'main', 'master']) {
38
+ if (tryGit(runGit, cwd, ['rev-parse', '--verify', '--quiet', candidate])) return candidate
39
+ }
40
+ return ''
41
+ }
42
+
43
+ function boundedReport(lines, maxChars) {
44
+ const kept = []
45
+ let length = 0
46
+ for (const raw of lines) {
47
+ const line = cleanLine(raw)
48
+ if (!line) continue
49
+ const nextLength = length + (kept.length ? 1 : 0) + line.length
50
+ if (nextLength > maxChars) {
51
+ const suffix = '… output shortened'
52
+ if (length + (kept.length ? 1 : 0) + suffix.length <= maxChars) kept.push(suffix)
53
+ break
54
+ }
55
+ kept.push(line)
56
+ length = nextLength
57
+ }
58
+ return kept.join('\n')
59
+ }
60
+
61
+ export function gitDiffReport({
62
+ cwd = process.cwd(),
63
+ runGit = defaultRunGit,
64
+ maxFiles = DEFAULT_MAX_FILES,
65
+ maxCommits = DEFAULT_MAX_COMMITS,
66
+ maxChars = DEFAULT_MAX_CHARS,
67
+ } = {}) {
68
+ gitText(runGit, cwd, ['rev-parse', '--show-toplevel'])
69
+
70
+ const status = tryGit(runGit, cwd, ['status', '--porcelain=v1', '--untracked-files=normal'])
71
+ const statusLines = status ? status.split(/\r?\n/).filter(Boolean) : []
72
+ const branch = tryGit(runGit, cwd, ['symbolic-ref', '--quiet', '--short', 'HEAD']) || 'detached HEAD'
73
+ const head = tryGit(runGit, cwd, ['rev-parse', '--short=8', 'HEAD']) || 'unknown'
74
+ const subject = cleanLine(tryGit(runGit, cwd, ['log', '-1', '--pretty=%s']), 160)
75
+ const comparisonRef = resolveComparisonRef(runGit, cwd)
76
+ const lines = []
77
+
78
+ if (statusLines.length) {
79
+ lines.push(`${plural(statusLines.length, 'uncommitted file')}`)
80
+ for (const line of statusLines.slice(0, Math.max(0, maxFiles))) lines.push(line)
81
+ if (statusLines.length > maxFiles) lines.push(`… ${plural(statusLines.length - maxFiles, 'more file')}`)
82
+ } else {
83
+ lines.push('No uncommitted changes')
84
+ }
85
+
86
+ lines.push(`Branch · ${branch} · ${head}`)
87
+
88
+ if (!comparisonRef) {
89
+ if (subject) lines.push(`Latest commit · ${head} — ${subject}`)
90
+ return boundedReport(lines, maxChars)
91
+ }
92
+
93
+ const counts = tryGit(runGit, cwd, ['rev-list', '--left-right', '--count', `${comparisonRef}...HEAD`])
94
+ .split(/\s+/)
95
+ .map((value) => Number(value))
96
+ const behind = Number.isFinite(counts[0]) ? counts[0] : 0
97
+ const ahead = Number.isFinite(counts[1]) ? counts[1] : 0
98
+
99
+ if (ahead > 0) {
100
+ lines.push(`${plural(ahead, 'commit')} ahead of ${comparisonRef}${behind ? ` · ${plural(behind, 'commit')} behind` : ''}`)
101
+ const commitLines = tryGit(runGit, cwd, ['log', `--max-count=${Math.max(0, maxCommits)}`, '--pretty=%h %s', `${comparisonRef}..HEAD`])
102
+ .split(/\r?\n/)
103
+ .filter(Boolean)
104
+ if (commitLines.length) {
105
+ lines.push(`Commits not in ${comparisonRef}`)
106
+ lines.push(...commitLines)
107
+ if (ahead > commitLines.length) lines.push(`… ${plural(ahead - commitLines.length, 'more commit')}`)
108
+ }
109
+ } else if (behind > 0) {
110
+ if (subject) lines.push(`Latest commit · ${head} — ${subject} · already in ${comparisonRef}`)
111
+ lines.push(`${plural(behind, 'commit')} behind ${comparisonRef}`)
112
+ } else {
113
+ if (subject) lines.push(`Latest commit · ${head} — ${subject}`)
114
+ lines.push(`Up to date with ${comparisonRef}`)
115
+ }
116
+
117
+ return boundedReport(lines, maxChars)
118
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.338",
3
+ "version": "0.7.339",
4
4
  "description": "Connect Claude Code, Codex, or Hermes on your computer to a Thinkpool Code room.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,6 +47,7 @@
47
47
  "hermes-delegation-guard.mjs",
48
48
  "runtime-registry.mjs",
49
49
  "command-catalog.mjs",
50
+ "git-diff-report.mjs",
50
51
  "runtime-session.mjs",
51
52
  "turn-stall.mjs",
52
53
  "update-gate.mjs",