heyquilt 0.0.1 → 0.3.90
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/LICENSE +85 -0
- package/README.md +630 -2
- package/RELEASES.md +733 -0
- package/assets/logo.svg +1 -0
- package/bin/quilt.js +816 -0
- package/desktop/main.js +315 -0
- package/package.json +163 -7
- package/src/account.js +563 -0
- package/src/admit-policy.js +32 -0
- package/src/agent-join.js +370 -0
- package/src/agent-task-workflow.js +176 -0
- package/src/agents/actions.js +85 -0
- package/src/agents/claude-code.js +180 -0
- package/src/agents/common.js +21 -0
- package/src/agents/cursor-transcripts.js +170 -0
- package/src/agents/cursor.js +404 -0
- package/src/agents/index.js +69 -0
- package/src/ai-vm-signin.js +98 -0
- package/src/ai-vm.js +380 -0
- package/src/api/access.js +75 -0
- package/src/api/activity.js +181 -0
- package/src/api/agent-auth.js +130 -0
- package/src/api/agent-placement.js +157 -0
- package/src/api/audit.js +83 -0
- package/src/api/auth.js +17 -0
- package/src/api/domains.js +30 -0
- package/src/api/file-paths.js +40 -0
- package/src/api/file-store.js +99 -0
- package/src/api/fly-machines.js +76 -0
- package/src/api/github-app.js +172 -0
- package/src/api/http.js +34 -0
- package/src/api/invite-email.js +53 -0
- package/src/api/issues.js +110 -0
- package/src/api/join-text.js +78 -0
- package/src/api/mailer.js +47 -0
- package/src/api/memory-store.js +1083 -0
- package/src/api/org-access.js +54 -0
- package/src/api/permissions.js +71 -0
- package/src/api/relay-cluster.js +251 -0
- package/src/api/relay-machines.js +60 -0
- package/src/api/routes/access-types.js +54 -0
- package/src/api/routes/agent-invites.js +115 -0
- package/src/api/routes/agents.js +157 -0
- package/src/api/routes/ai-vm.js +369 -0
- package/src/api/routes/github.js +380 -0
- package/src/api/routes/grants.js +70 -0
- package/src/api/routes/invites.js +188 -0
- package/src/api/routes/issues.js +47 -0
- package/src/api/routes/join.js +167 -0
- package/src/api/routes/members.js +110 -0
- package/src/api/routes/orgs.js +177 -0
- package/src/api/routes/relay-cluster.js +90 -0
- package/src/api/routes/relay.js +76 -0
- package/src/api/routes/session-invites.js +119 -0
- package/src/api/routes/sessions.js +92 -0
- package/src/api/routes/teams.js +132 -0
- package/src/api/routes/workspace-agents.js +332 -0
- package/src/api/routes/workspace-files.js +277 -0
- package/src/api/routes/workspace-invites.js +196 -0
- package/src/api/routes/workspaces.js +225 -0
- package/src/api/server.js +623 -0
- package/src/api/slugs.js +20 -0
- package/src/api/supabase-store.js +765 -0
- package/src/api/team-access.js +30 -0
- package/src/api/tokens.js +31 -0
- package/src/api/workspace-access.js +62 -0
- package/src/api/workspace-reach.js +60 -0
- package/src/autowork-tools.js +166 -0
- package/src/autowork.js +387 -0
- package/src/blobstore.js +99 -0
- package/src/boxes.js +37 -0
- package/src/branchdocs.js +235 -0
- package/src/branches.js +194 -0
- package/src/catchup.js +111 -0
- package/src/chat-archive.js +85 -0
- package/src/chat-links.js +923 -0
- package/src/clash.js +140 -0
- package/src/cloud-ai.js +50 -0
- package/src/commit-request.js +59 -0
- package/src/commit.js +213 -0
- package/src/connection.js +569 -0
- package/src/control.js +245 -0
- package/src/conversation.js +125 -0
- package/src/doctor.js +156 -0
- package/src/duties.js +303 -0
- package/src/editors.js +228 -0
- package/src/fsutil.js +87 -0
- package/src/git-hook.js +48 -0
- package/src/git.js +147 -0
- package/src/github-commit.js +320 -0
- package/src/gitignore.js +49 -0
- package/src/gitstate.js +900 -0
- package/src/history.js +307 -0
- package/src/hooks.js +371 -0
- package/src/hosted-agents.js +24 -0
- package/src/hosted-client.js +112 -0
- package/src/identity.js +99 -0
- package/src/inbox.js +237 -0
- package/src/integrations.js +221 -0
- package/src/key-cli.js +115 -0
- package/src/largefiles.js +91 -0
- package/src/legacy.js +25 -0
- package/src/mcp-proxy.js +40 -0
- package/src/mcp-room.js +32 -0
- package/src/mcp.js +1261 -0
- package/src/merge3.js +158 -0
- package/src/merges.js +148 -0
- package/src/pass-source.js +176 -0
- package/src/passes.js +64 -0
- package/src/pathrules.js +127 -0
- package/src/persona.js +101 -0
- package/src/presence.js +360 -0
- package/src/private-file.js +41 -0
- package/src/procs.js +70 -0
- package/src/project-rules.js +178 -0
- package/src/protocol.js +92 -0
- package/src/quiet-warnings.js +9 -0
- package/src/relay-ai-vm.js +82 -0
- package/src/relay-ask.js +141 -0
- package/src/relay-ci.js +111 -0
- package/src/relay-clash.js +597 -0
- package/src/relay-cluster.js +454 -0
- package/src/relay-commit.js +887 -0
- package/src/relay-filekey.js +46 -0
- package/src/relay-git.js +274 -0
- package/src/relay-githttp.js +137 -0
- package/src/relay-github.js +109 -0
- package/src/relay-lease.js +137 -0
- package/src/relay-lfs.js +37 -0
- package/src/relay-mcp.js +1679 -0
- package/src/relay-metrics.js +75 -0
- package/src/relay-outbound.js +1144 -0
- package/src/relay-sync.js +719 -0
- package/src/relay-turn-sync.js +307 -0
- package/src/relay-upstream.js +934 -0
- package/src/relay-webhooks.js +111 -0
- package/src/releases.js +131 -0
- package/src/report.js +129 -0
- package/src/runner.js +306 -0
- package/src/same-edit.js +18 -0
- package/src/server.js +4187 -0
- package/src/session-access.js +170 -0
- package/src/session-name.js +15 -0
- package/src/session.js +7361 -0
- package/src/settings.js +46 -0
- package/src/setup.js +184 -0
- package/src/shell-path.js +46 -0
- package/src/status.js +199 -0
- package/src/summarize.js +109 -0
- package/src/sync-words.js +42 -0
- package/src/task-comments.js +92 -0
- package/src/tasks.js +670 -0
- package/src/textdiff.js +20 -0
- package/src/turn-sync-plan.js +142 -0
- package/src/turn-sync.js +191 -0
- package/src/ui/access-form.js +37 -0
- package/src/ui/agent-env-key.js +76 -0
- package/src/ui/agent-guide.js +127 -0
- package/src/ui/agent-kinds.js +175 -0
- package/src/ui/agent-place.js +265 -0
- package/src/ui/app.css +2309 -0
- package/src/ui/app.js +542 -0
- package/src/ui/board.js +465 -0
- package/src/ui/boxes.js +76 -0
- package/src/ui/branches.js +111 -0
- package/src/ui/catchup.js +77 -0
- package/src/ui/changes.js +159 -0
- package/src/ui/chat.js +266 -0
- package/src/ui/cloud-ai-model.js +107 -0
- package/src/ui/cloud-ai.js +221 -0
- package/src/ui/common.js +431 -0
- package/src/ui/csv.js +21 -0
- package/src/ui/diffview.js +74 -0
- package/src/ui/feed-convs.js +39 -0
- package/src/ui/feed.js +174 -0
- package/src/ui/files.js +579 -0
- package/src/ui/fileview.js +123 -0
- package/src/ui/git-rule.js +60 -0
- package/src/ui/home.js +1323 -0
- package/src/ui/index.html +16 -0
- package/src/ui/invite.js +97 -0
- package/src/ui/invites.js +79 -0
- package/src/ui/loom-model.js +516 -0
- package/src/ui/loom.css +349 -0
- package/src/ui/loom.js +753 -0
- package/src/ui/mark.js +104 -0
- package/src/ui/merges.js +158 -0
- package/src/ui/rail-model.js +78 -0
- package/src/ui/rail.js +173 -0
- package/src/ui/releases.js +182 -0
- package/src/ui/schedule.js +156 -0
- package/src/ui/session.js +3394 -0
- package/src/ui/signin.js +75 -0
- package/src/ui/sounds.js +110 -0
- package/src/ui/tool-logo.js +86 -0
- package/src/ui/tree.js +276 -0
- package/src/ui/workspaces.js +542 -0
- package/src/ui-server.js +1272 -0
- package/src/update-check.js +67 -0
- package/src/upstream.js +155 -0
- package/src/webhooks.js +235 -0
- package/src/workspace-cli.js +62 -0
- package/src/workspace-tools.js +340 -0
- package/index.js +0 -2
package/src/history.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// The chronology of a session: every change to a shared file, with who made it,
|
|
2
|
+
// when, what changed (a capped unified diff) and the task it was for. Kept in
|
|
3
|
+
// the shared doc (`history`) so everyone, including hosted agents, reads the
|
|
4
|
+
// same record; queried with quilt_history / `quilt history`.
|
|
5
|
+
import crypto from 'node:crypto'
|
|
6
|
+
import { globMatcher } from './pathrules.js'
|
|
7
|
+
|
|
8
|
+
export const HISTORY_CAP = 2000 // entries
|
|
9
|
+
export const MAX_HISTORY_CHARS = 1_500_000 // total diff text kept
|
|
10
|
+
export const MAX_DIFF_CHARS = 6000 // per entry
|
|
11
|
+
export const BURST_MS = 20000 // saves closer than this, same person and file, fold into one entry
|
|
12
|
+
const CONTEXT = 2
|
|
13
|
+
const MAX_DP_CELLS = 2_000_000 // beyond this the middle of a diff is shown coarsely
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A unified-style line diff ('' when equal). Common leading and trailing lines
|
|
17
|
+
* are skipped, the middle gets an LCS alignment when it is small enough and a
|
|
18
|
+
* coarse "all removed, all added" block otherwise. Capped at MAX_DIFF_CHARS.
|
|
19
|
+
*/
|
|
20
|
+
export function lineDiff (before, after, { context = CONTEXT, max = MAX_DIFF_CHARS } = {}) {
|
|
21
|
+
before = String(before ?? ''); after = String(after ?? '')
|
|
22
|
+
if (before === after) return ''
|
|
23
|
+
const a = splitLines(before)
|
|
24
|
+
const b = splitLines(after)
|
|
25
|
+
let head = 0
|
|
26
|
+
while (head < a.length && head < b.length && a[head] === b[head]) head++
|
|
27
|
+
let tail = 0
|
|
28
|
+
while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) tail++
|
|
29
|
+
const ma = a.slice(head, a.length - tail)
|
|
30
|
+
const mb = b.slice(head, b.length - tail)
|
|
31
|
+
const ops = align(ma, mb) // [kind, text] with kind in '=', '-', '+'
|
|
32
|
+
const full = [...a.slice(0, head).map((l) => ['=', l]), ...ops, ...a.slice(a.length - tail).map((l) => ['=', l])]
|
|
33
|
+
return cap(hunks(full, context), max)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function splitLines (s) {
|
|
37
|
+
if (!s) return []
|
|
38
|
+
const lines = s.split('\n')
|
|
39
|
+
if (lines.at(-1) === '') lines.pop()
|
|
40
|
+
return lines
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function align (a, b) {
|
|
44
|
+
if (!a.length) return b.map((l) => ['+', l])
|
|
45
|
+
if (!b.length) return a.map((l) => ['-', l])
|
|
46
|
+
if (a.length * b.length > MAX_DP_CELLS) return [...a.map((l) => ['-', l]), ...b.map((l) => ['+', l])]
|
|
47
|
+
const n = a.length; const m = b.length
|
|
48
|
+
const dp = new Uint32Array((n + 1) * (m + 1))
|
|
49
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
50
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
51
|
+
dp[i * (m + 1) + j] = a[i] === b[j] ? dp[(i + 1) * (m + 1) + j + 1] + 1 : Math.max(dp[(i + 1) * (m + 1) + j], dp[i * (m + 1) + j + 1])
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const out = []
|
|
55
|
+
let i = 0; let j = 0
|
|
56
|
+
while (i < n && j < m) {
|
|
57
|
+
if (a[i] === b[j]) { out.push(['=', a[i]]); i++; j++ } else if (dp[(i + 1) * (m + 1) + j] >= dp[i * (m + 1) + j + 1]) out.push(['-', a[i++]])
|
|
58
|
+
else out.push(['+', b[j++]])
|
|
59
|
+
}
|
|
60
|
+
while (i < n) out.push(['-', a[i++]])
|
|
61
|
+
while (j < m) out.push(['+', b[j++]])
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Groups ops into hunks with `context` equal lines around each change. */
|
|
66
|
+
function hunks (ops, context) {
|
|
67
|
+
const out = []
|
|
68
|
+
let i = 0
|
|
69
|
+
let oldLine = 1; let newLine = 1
|
|
70
|
+
while (i < ops.length) {
|
|
71
|
+
if (ops[i][0] === '=') { oldLine++; newLine++; i++; continue }
|
|
72
|
+
// A change starts here. Walk forward until a stretch of > 2*context equal lines or the end.
|
|
73
|
+
let start = i; let end = i
|
|
74
|
+
let equalRun = 0
|
|
75
|
+
for (let k = i; k < ops.length; k++) {
|
|
76
|
+
if (ops[k][0] === '=') { equalRun++; if (equalRun > 2 * context) break } else { equalRun = 0; end = k }
|
|
77
|
+
}
|
|
78
|
+
const from = Math.max(0, start - context)
|
|
79
|
+
const to = Math.min(ops.length, end + 1 + context)
|
|
80
|
+
// Lines before `from` since the last hunk are all equal: advance the counters.
|
|
81
|
+
const lead = start - from
|
|
82
|
+
const oldStart = oldLine - lead
|
|
83
|
+
const newStart = newLine - lead
|
|
84
|
+
let oldCount = 0; let newCount = 0
|
|
85
|
+
const body = []
|
|
86
|
+
for (let k = from; k < to; k++) {
|
|
87
|
+
const [kind, text] = ops[k]
|
|
88
|
+
if (kind !== '+') oldCount++
|
|
89
|
+
if (kind !== '-') newCount++
|
|
90
|
+
body.push((kind === '=' ? ' ' : kind) + text)
|
|
91
|
+
}
|
|
92
|
+
out.push(`@@ -${oldCount ? oldStart : 0},${oldCount} +${newCount ? newStart : 0},${newCount} @@`, ...body)
|
|
93
|
+
for (let k = i; k < to; k++) {
|
|
94
|
+
if (ops[k][0] !== '+') oldLine++
|
|
95
|
+
if (ops[k][0] !== '-') newLine++
|
|
96
|
+
}
|
|
97
|
+
i = to
|
|
98
|
+
}
|
|
99
|
+
return out.join('\n')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cap (text, max) {
|
|
103
|
+
if (text.length <= max) return text
|
|
104
|
+
const cut = text.lastIndexOf('\n', max)
|
|
105
|
+
return `${text.slice(0, cut > 0 ? cut : max)}\n… (diff truncated)`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function counts (diff) {
|
|
109
|
+
let added = 0; let removed = 0
|
|
110
|
+
for (const line of diff.split('\n')) {
|
|
111
|
+
if (line.startsWith('+')) added++
|
|
112
|
+
else if (line.startsWith('-')) removed++
|
|
113
|
+
}
|
|
114
|
+
return { added, removed }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Writes entries into the shared `history` array. One instance per process;
|
|
119
|
+
* it remembers the text a burst started from so later saves in the same burst
|
|
120
|
+
* replace the entry with a diff spanning the whole burst.
|
|
121
|
+
*/
|
|
122
|
+
export class HistoryLog {
|
|
123
|
+
constructor (doc, array, { origin = 'history', burstMs = BURST_MS } = {}) {
|
|
124
|
+
this.doc = doc
|
|
125
|
+
this.array = array
|
|
126
|
+
this.origin = origin
|
|
127
|
+
this.burstMs = burstMs
|
|
128
|
+
this.bursts = new Map() // "by\0path" -> { id, before, ts }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
entries () { return this.array.toArray().filter((e) => e && typeof e === 'object' && e.path) }
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {object} c
|
|
135
|
+
* @param {string} c.by who changed it
|
|
136
|
+
* @param {string} c.path
|
|
137
|
+
* @param {'created'|'edited'|'deleted'} c.kind
|
|
138
|
+
* @param {string} [c.before] text before (text files)
|
|
139
|
+
* @param {string} [c.after] text after
|
|
140
|
+
* @param {string} [c.detail] "N bytes" for binaries; derived from the diff otherwise
|
|
141
|
+
* @param {{id:string,title:string}|null} [c.task] the task this change was for
|
|
142
|
+
* @param {boolean} [c.pulled] a git pull brought this change (it is not the person's own edit)
|
|
143
|
+
* @param {number} [c.ts]
|
|
144
|
+
*/
|
|
145
|
+
record ({ by, path, kind, before, after, detail, task, pulled = false, seed = false, ts = Date.now() }) {
|
|
146
|
+
const key = `${by}\0${path}`
|
|
147
|
+
const burst = this.bursts.get(key)
|
|
148
|
+
const text = typeof before === 'string' || typeof after === 'string'
|
|
149
|
+
// A save soon after a file's starting copy (seed) folds into it as the editor's own change: the entry is then theirs, not a seed.
|
|
150
|
+
// Never over someone else's change to the file in between (any process's, in the shared log): the fold's diff would credit their lines to `by`.
|
|
151
|
+
const foldable = kind === 'edited' && burst && ts - burst.ts < this.burstMs && text && !pulled && !seed && !burst.pulled && this.lastIdFor(path) === burst.id
|
|
152
|
+
const base = foldable ? burst.before : (before ?? '')
|
|
153
|
+
const diff = text ? lineDiff(base, after ?? '') : ''
|
|
154
|
+
const c = text ? counts(diff) : { added: 0, removed: 0 }
|
|
155
|
+
const entry = {
|
|
156
|
+
id: foldable ? burst.id : crypto.randomBytes(6).toString('hex'),
|
|
157
|
+
by: String(by || ''),
|
|
158
|
+
path: String(path || ''),
|
|
159
|
+
kind: foldable && burst.kind === 'created' ? 'created' : kind, // a file created and then saved again is still "created"
|
|
160
|
+
ts,
|
|
161
|
+
from: foldable ? burst.from : ts,
|
|
162
|
+
added: c.added,
|
|
163
|
+
removed: c.removed,
|
|
164
|
+
detail: text ? `+${c.added} -${c.removed}` : String(detail || ''),
|
|
165
|
+
task: task && task.id ? { id: String(task.id), title: String(task.title || '') } : null,
|
|
166
|
+
...(pulled ? { pulled: true } : {}),
|
|
167
|
+
...(seed ? { seed: true } : {}),
|
|
168
|
+
diff
|
|
169
|
+
}
|
|
170
|
+
this.doc.transact(() => {
|
|
171
|
+
if (foldable) {
|
|
172
|
+
const i = this.indexOf(entry.id)
|
|
173
|
+
if (i >= 0) this.array.delete(i, 1)
|
|
174
|
+
this.array.insert(i >= 0 ? i : this.array.length, [entry])
|
|
175
|
+
} else {
|
|
176
|
+
this.array.push([entry])
|
|
177
|
+
}
|
|
178
|
+
this.trim()
|
|
179
|
+
}, this.origin)
|
|
180
|
+
if (kind === 'deleted') this.bursts.delete(key)
|
|
181
|
+
else this.bursts.set(key, { id: entry.id, kind: entry.kind, before: base, from: entry.from, ts, pulled, seed })
|
|
182
|
+
return entry
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The id of the newest entry for `path` in the shared log, or null. */
|
|
186
|
+
lastIdFor (path) {
|
|
187
|
+
for (let i = this.array.length - 1; i >= 0; i--) {
|
|
188
|
+
const e = this.array.get(i)
|
|
189
|
+
if (e && e.path === path) return e.id
|
|
190
|
+
}
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
indexOf (id) {
|
|
195
|
+
for (let i = this.array.length - 1; i >= 0; i--) {
|
|
196
|
+
const e = this.array.get(i)
|
|
197
|
+
if (e && e.id === id) return i
|
|
198
|
+
}
|
|
199
|
+
return -1
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
trim () {
|
|
203
|
+
if (this.array.length > HISTORY_CAP) this.array.delete(0, this.array.length - HISTORY_CAP)
|
|
204
|
+
let total = 0
|
|
205
|
+
const all = this.array.toArray()
|
|
206
|
+
for (const e of all) total += (e && e.diff ? e.diff.length : 0) + 120
|
|
207
|
+
let drop = 0
|
|
208
|
+
while (total > MAX_HISTORY_CHARS && drop < all.length - 1) {
|
|
209
|
+
const e = all[drop++]
|
|
210
|
+
total -= (e && e.diff ? e.diff.length : 0) + 120
|
|
211
|
+
}
|
|
212
|
+
if (drop) this.array.delete(0, drop)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* "2h", "45m", "3d", "1w", "today", "yesterday" or a date. Returns a timestamp,
|
|
218
|
+
* null for empty input, undefined when it can't be read.
|
|
219
|
+
*/
|
|
220
|
+
export function parseSince (s, now = Date.now()) {
|
|
221
|
+
const str = String(s ?? '').trim().toLowerCase()
|
|
222
|
+
if (!str) return null
|
|
223
|
+
const m = /^(\d+(?:\.\d+)?)\s*(s|sec|m|min|h|hr|hour|d|day|w|week)s?$/.exec(str)
|
|
224
|
+
if (m) {
|
|
225
|
+
const unit = { s: 1e3, sec: 1e3, m: 60e3, min: 60e3, h: 3600e3, hr: 3600e3, hour: 3600e3, d: 86400e3, day: 86400e3, w: 7 * 86400e3, week: 7 * 86400e3 }[m[2]]
|
|
226
|
+
return now - Math.round(+m[1] * unit)
|
|
227
|
+
}
|
|
228
|
+
const midnight = new Date(now).setHours(0, 0, 0, 0)
|
|
229
|
+
if (str === 'today') return midnight
|
|
230
|
+
if (str === 'yesterday') return midnight - 86400e3
|
|
231
|
+
const t = Date.parse(s)
|
|
232
|
+
return Number.isNaN(t) ? undefined : t
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Filters history entries. `path` is an exact path, a folder prefix ending in
|
|
237
|
+
* "/", or a glob. `by` is a name (case-insensitive). `since` is a timestamp.
|
|
238
|
+
* Returns oldest first; `limit` keeps the newest.
|
|
239
|
+
*/
|
|
240
|
+
export function queryHistory (entries, { path, by, since, task, limit = 50 } = {}) {
|
|
241
|
+
let list = entries.filter((e) => e && e.path)
|
|
242
|
+
if (path) {
|
|
243
|
+
const p = String(path).replace(/\\/g, '/').replace(/^\.\//, '')
|
|
244
|
+
const match = /[*?[{]/.test(p) ? globMatcher(p) : p.endsWith('/') ? (x) => x.startsWith(p) : (x) => x === p
|
|
245
|
+
list = list.filter((e) => match(e.path))
|
|
246
|
+
}
|
|
247
|
+
if (by) { const who = String(by).trim().toLowerCase(); list = list.filter((e) => String(e.by).toLowerCase() === who) }
|
|
248
|
+
if (task) list = list.filter((e) => e.task && e.task.id === task)
|
|
249
|
+
if (typeof since === 'number') list = list.filter((e) => e.ts >= since)
|
|
250
|
+
list.sort((a, b) => a.ts - b.ts)
|
|
251
|
+
if (limit && list.length > limit) list = list.slice(-limit)
|
|
252
|
+
return list
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function ago (ts, now) {
|
|
256
|
+
const s = Math.max(0, Math.round((now - ts) / 1000))
|
|
257
|
+
if (s < 60) return `${s}s ago`
|
|
258
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ago`
|
|
259
|
+
if (s < 172800) return `${Math.floor(s / 3600)}h ago`
|
|
260
|
+
return `${Math.floor(s / 86400)}d ago`
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** One line per change, oldest first; with `withDiff`, each diff follows its line. */
|
|
264
|
+
export function formatHistory (list, { withDiff = false, now = Date.now() } = {}) {
|
|
265
|
+
if (!list.length) return 'No changes match.'
|
|
266
|
+
const out = []
|
|
267
|
+
for (const e of list) {
|
|
268
|
+
const span = e.from && e.ts - e.from > 60000 ? ` over ${Math.round((e.ts - e.from) / 60000)}m` : ''
|
|
269
|
+
const what = e.detail ? ` (${e.detail})` : ''
|
|
270
|
+
const task = e.task ? ` for "${e.task.title}" [${e.task.id}]` : ''
|
|
271
|
+
out.push(`[${ago(e.ts, now)}] ${e.by} ${e.kind} ${e.path}${what}${e.pulled ? ' by pulling from git' : ''}${span}${task}`)
|
|
272
|
+
if (withDiff && e.diff) out.push(e.diff, '')
|
|
273
|
+
}
|
|
274
|
+
return out.join('\n').replace(/\n+$/, '')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Who made the changes between `before` (a file as git has it) and `after` (as it is now): each
|
|
279
|
+
* line that differs is put down to the newest history entry (`entries`: that file's, oldest
|
|
280
|
+
* first) that added or removed it. Pulls from git are nobody's, and lines too common to tell
|
|
281
|
+
* apart (blank, a lone brace) are not counted. Names in the order their lines come. Unlike "who
|
|
282
|
+
* edited it since", someone whose edit was later undone, or who only opened the session with the
|
|
283
|
+
* file, is not named.
|
|
284
|
+
*/
|
|
285
|
+
export function blameChange ({ before = '', after = '', entries = [] }) {
|
|
286
|
+
const diff = lineDiff(before, after, { context: 0, max: 400_000 })
|
|
287
|
+
if (!diff) return []
|
|
288
|
+
const changed = diff.split('\n').filter((l) => (l[0] === '+' || l[0] === '-') && !/^(\+\+\+|---) /.test(l) && l.slice(1).replace(/[\s{}()[\];,]/g, '').length >= 2)
|
|
289
|
+
const newest = [...entries].reverse().filter((e) => e && !e.pulled && !e.seed && e.by && typeof e.diff === 'string').map((e) => ({ by: e.by, lines: new Set(e.diff.split('\n')) }))
|
|
290
|
+
const out = []
|
|
291
|
+
for (const line of changed) {
|
|
292
|
+
const e = newest.find((x) => x.lines.has(line))
|
|
293
|
+
if (e && !out.includes(e.by)) out.push(e.by)
|
|
294
|
+
}
|
|
295
|
+
return out
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The In-progress task assigned to `name` (the person or their AI): the first on
|
|
300
|
+
* the board, with the AI's tasks first when `preferAi` says the AI is the one working.
|
|
301
|
+
*/
|
|
302
|
+
export function currentTask (tasks, name, { preferAi = false } = {}) {
|
|
303
|
+
const doing = (tasks || []).filter((t) => t && !t.archived && t.column === 'doing' && t.assignee === name)
|
|
304
|
+
if (!doing.length) return null
|
|
305
|
+
doing.sort((a, b) => (Number(!!b.forAi === preferAi) - Number(!!a.forAi === preferAi)) || ((a.order ?? 0) - (b.order ?? 0)))
|
|
306
|
+
return { id: doing[0].id, title: doing[0].title }
|
|
307
|
+
}
|