@loicngr/kobo 1.7.15 → 1.7.17

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 (86) hide show
  1. package/AGENTS.md +35 -4
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +2 -1
  4. package/dist/mcp-server/kobo-tasks-handlers.js +48 -5
  5. package/dist/mcp-server/kobo-tasks-server.js +8 -20
  6. package/dist/server/db/migrations.js +19 -0
  7. package/dist/server/db/schema.js +9 -0
  8. package/dist/server/routes/settings.js +2 -0
  9. package/dist/server/routes/workspaces.js +242 -56
  10. package/dist/server/services/agent/engines/claude-code/engine.js +59 -7
  11. package/dist/server/services/agent/engines/claude-code/event-mapper.js +7 -1
  12. package/dist/server/services/agent/engines/claude-code/options-builder.js +2 -2
  13. package/dist/server/services/agent/engines/codex/options-builder.js +2 -2
  14. package/dist/server/services/agent/orchestrator.js +1 -0
  15. package/dist/server/services/change-source-branch-service.js +150 -0
  16. package/dist/server/services/chat-history-service.js +41 -0
  17. package/dist/server/services/file-editor-service.js +59 -0
  18. package/dist/server/services/forge/github/provider.js +121 -0
  19. package/dist/server/services/forge/gitlab/provider.js +178 -0
  20. package/dist/server/services/forge/none.js +23 -0
  21. package/dist/server/services/forge/registry.js +17 -0
  22. package/dist/server/services/forge/resolve.js +34 -0
  23. package/dist/server/services/forge/types.js +9 -0
  24. package/dist/server/services/git-stats-service.js +32 -0
  25. package/dist/server/services/pr-watcher-service.js +33 -3
  26. package/dist/server/services/settings-defaults.js +77 -0
  27. package/dist/server/services/settings-service.js +34 -0
  28. package/dist/server/utils/git-ops.js +121 -134
  29. package/package.json +1 -1
  30. package/src/client/dist/spa/assets/ActivityFeed-CDhLuhI3.js +8 -0
  31. package/src/client/dist/spa/assets/{ClosePopup-CxvZA3ft.js → ClosePopup-A-tSm4aa.js} +1 -1
  32. package/src/client/dist/spa/assets/{CreatePage-CdZr7f3j.js → CreatePage-xIjxPliD.js} +1 -1
  33. package/src/client/dist/spa/assets/DiffViewer-C4L5y8Ho.css +1 -0
  34. package/src/client/dist/spa/assets/DiffViewer-CcgF65Mo.js +8 -0
  35. package/src/client/dist/spa/assets/{HealthPage-z1uIOpYk.js → HealthPage-Bw-9__wY.js} +1 -1
  36. package/src/client/dist/spa/assets/MainLayout-C2ULeep-.js +37 -0
  37. package/src/client/dist/spa/assets/{MainLayout-BJmBXwYn.css → MainLayout-KEr19FOv.css} +1 -1
  38. package/src/client/dist/spa/assets/{QExpansionItem-BTd5m2yV.js → QExpansionItem-CgJQdznK.js} +1 -1
  39. package/src/client/dist/spa/assets/{QMenu-C2Wwwf2E.js → QMenu-NVDU7D3u.js} +1 -1
  40. package/src/client/dist/spa/assets/{QScrollArea-A1wI0IXU.js → QScrollArea-_Ji1cgqL.js} +1 -1
  41. package/src/client/dist/spa/assets/{QTooltip-Bfdmzm_m.js → QTooltip-BC7PnZJ1.js} +1 -1
  42. package/src/client/dist/spa/assets/{SearchPage-ChmKHNKn.js → SearchPage-D2x2X7K7.js} +1 -1
  43. package/src/client/dist/spa/assets/SettingsPage-BLb9B9iY.js +9 -0
  44. package/src/client/dist/spa/assets/{SettingsPage-BJLyYrBN.css → SettingsPage-BTGPZaqC.css} +1 -1
  45. package/src/client/dist/spa/assets/{TouchPan-BIE5rs7U.js → TouchPan-D0fJnlOC.js} +1 -1
  46. package/src/client/dist/spa/assets/WorkspacePage-BlAVs03z.js +4 -0
  47. package/src/client/dist/spa/assets/{WorkspacePage-tFBswKV9.css → WorkspacePage-DTV0oWHS.css} +1 -1
  48. package/src/client/dist/spa/assets/{build-path-tree-BGUV3nY1.js → build-path-tree-CyqReJkk.js} +1 -1
  49. package/src/client/dist/spa/assets/{cssMode-BU4X8R6a.js → cssMode-BsT_HBz-.js} +1 -1
  50. package/src/client/dist/spa/assets/{editor.api-B4xBDzmJ.js → editor.api-CIxiApSC.js} +1 -1
  51. package/src/client/dist/spa/assets/{editor.main-CSZRkloL.js → editor.main-D-1-e3_n.js} +3 -3
  52. package/src/client/dist/spa/assets/{engineFeatures-CLOVr5b4.js → engineFeatures-baMvMT98.js} +1 -1
  53. package/src/client/dist/spa/assets/{expand-template-BxUkuL5g.js → expand-template-CF0lBr4L.js} +1 -1
  54. package/src/client/dist/spa/assets/{freemarker2-DRz20wAV.js → freemarker2-q2PyKiM2.js} +1 -1
  55. package/src/client/dist/spa/assets/{handlebars-C0dsvPnC.js → handlebars-DoaZIK6r.js} +1 -1
  56. package/src/client/dist/spa/assets/{html-Cqvj1pWs.js → html-DHcse-fd.js} +1 -1
  57. package/src/client/dist/spa/assets/{htmlMode-BTHNvkm6.js → htmlMode-DPZCU7DB.js} +1 -1
  58. package/src/client/dist/spa/assets/i18n-DwzfgKc3.js +1 -0
  59. package/src/client/dist/spa/assets/index-DMUFfCIq.js +52 -0
  60. package/src/client/dist/spa/assets/{javascript-C8n3U02v.js → javascript-Ddw7c3eO.js} +1 -1
  61. package/src/client/dist/spa/assets/{jsonMode-C3AFxQ6K.js → jsonMode-vmAmvg_N.js} +1 -1
  62. package/src/client/dist/spa/assets/{kobo-commands-BuxgteGZ.js → kobo-commands-B2AhWe1S.js} +1 -1
  63. package/src/client/dist/spa/assets/{liquid-C4wtUDrJ.js → liquid-Bwz3vr4k.js} +1 -1
  64. package/src/client/dist/spa/assets/{mdx-CaT1p1F2.js → mdx-B2uBtVef.js} +1 -1
  65. package/src/client/dist/spa/assets/{monaco.contribution-CJg5GKVf.js → monaco.contribution-B3cRHiXp.js} +2 -2
  66. package/src/client/dist/spa/assets/{notifications-BC6en6Lt.js → notifications-Hq-6rEYv.js} +1 -1
  67. package/src/client/dist/spa/assets/{permissionModes-BQHBTBwa.js → permissionModes-DuwIe4ty.js} +1 -1
  68. package/src/client/dist/spa/assets/{python-Cj54W2Tg.js → python-D5ykDuc8.js} +1 -1
  69. package/src/client/dist/spa/assets/{razor-D3gJxoX_.js → razor-CStvtec5.js} +1 -1
  70. package/src/client/dist/spa/assets/{render-chat-markdown-DxEHr3lW.js → render-chat-markdown-BywKNkXe.js} +1 -1
  71. package/src/client/dist/spa/assets/{tsMode-B6S4PLWH.js → tsMode-WEgYYKFt.js} +1 -1
  72. package/src/client/dist/spa/assets/{typescript-Ca8AEX3t.js → typescript-BzFHuirT.js} +1 -1
  73. package/src/client/dist/spa/assets/{use-onboarding-CNeLPDtv.js → use-onboarding-C98jCHZu.js} +1 -1
  74. package/src/client/dist/spa/assets/{xml-CsKo4k8C.js → xml-NuCdCQMI.js} +1 -1
  75. package/src/client/dist/spa/assets/{yaml-X5yKmi6z.js → yaml-CkTTgcUh.js} +1 -1
  76. package/src/client/dist/spa/index.html +2 -2
  77. package/src/mcp-server/kobo-tasks-handlers.ts +56 -5
  78. package/src/mcp-server/kobo-tasks-server.ts +8 -19
  79. package/src/client/dist/spa/assets/ActivityFeed-DU6lDEP0.js +0 -8
  80. package/src/client/dist/spa/assets/DiffViewer-DTdDcKZC.css +0 -1
  81. package/src/client/dist/spa/assets/DiffViewer-m801GPfI.js +0 -7
  82. package/src/client/dist/spa/assets/MainLayout-oJdQ-QKM.js +0 -37
  83. package/src/client/dist/spa/assets/SettingsPage-B59LoCos.js +0 -9
  84. package/src/client/dist/spa/assets/WorkspacePage-Bj1PJSWT.js +0 -4
  85. package/src/client/dist/spa/assets/i18n-D1I-Us2H.js +0 -1
  86. package/src/client/dist/spa/assets/index-KABmOIkF.js +0 -2
@@ -1,5 +1,8 @@
1
- import { getPrStatusAsync } from '../utils/git-ops.js';
1
+ import { fetchSourceBranchAsync } from '../utils/git-ops.js';
2
2
  import { stopDevServer } from './dev-server-service.js';
3
+ import { getForgeProvider } from './forge/registry.js';
4
+ import { resolveForge } from './forge/resolve.js';
5
+ import { computeGitStats } from './git-stats-service.js';
3
6
  import { destroyTerminal } from './terminal-service.js';
4
7
  import { emitEphemeral } from './websocket-service.js';
5
8
  import { archiveWorkspace, getWorkspace, listWorkspaces, updateWorkspaceSourceBranch } from './workspace-service.js';
@@ -17,6 +20,8 @@ let checking = false;
17
20
  /** Tracks the last known PR snapshot per workspace, used to detect transitions
18
21
  * (state, base, reviewDecision). */
19
22
  const lastKnownPr = new Map();
23
+ /** Latest git-stats snapshot per workspace, refreshed each watcher tick. */
24
+ const lastKnownGitStats = new Map();
20
25
  /**
21
26
  * Read-only snapshot map, keyed by workspace id. Used by the drawer indicator
22
27
  * AND the Git panel. Workspaces without a known PR are absent.
@@ -28,12 +33,22 @@ export function getAllPrSnapshots() {
28
33
  }
29
34
  return out;
30
35
  }
36
+ /** Read-only git-stats map, keyed by workspace id. Used by the bulk
37
+ * `/api/workspaces/info` endpoint. */
38
+ export function getAllGitStats() {
39
+ const out = {};
40
+ for (const [id, s] of lastKnownGitStats) {
41
+ out[id] = s;
42
+ }
43
+ return out;
44
+ }
31
45
  /**
32
46
  * Test-only escape hatch — drops the in-memory cache so each test starts
33
47
  * from a clean slate. Not part of the public API.
34
48
  */
35
49
  export function _resetForTest() {
36
50
  lastKnownPr.clear();
51
+ lastKnownGitStats.clear();
37
52
  }
38
53
  export async function checkPrStatuses() {
39
54
  const workspaces = listWorkspaces(false); // non-archived only
@@ -43,9 +58,24 @@ export async function checkPrStatuses() {
43
58
  lastKnownPr.delete(id);
44
59
  }
45
60
  }
61
+ for (const id of lastKnownGitStats.keys()) {
62
+ if (!workspaces.some((ws) => ws.id === id)) {
63
+ lastKnownGitStats.delete(id);
64
+ }
65
+ }
46
66
  for (const ws of workspaces) {
47
67
  try {
48
- const pr = await getPrStatusAsync(ws.projectPath, ws.workingBranch);
68
+ const pr = await getForgeProvider(resolveForge(ws.projectPath)).getPrStatus(ws.worktreePath, ws.workingBranch);
69
+ // Git stats — best-effort, cached independently of the PR-transition
70
+ // logic below. Its own try/catch so a git failure neither skips PR
71
+ // transitions nor poisons other workspaces.
72
+ try {
73
+ void fetchSourceBranchAsync(ws.worktreePath, ws.sourceBranch).catch(() => { });
74
+ lastKnownGitStats.set(ws.id, await computeGitStats(ws, pr));
75
+ }
76
+ catch (err) {
77
+ console.error(`[pr-watcher] computeGitStats failed for '${ws.name}':`, err instanceof Error ? err.message : err);
78
+ }
49
79
  if (!pr)
50
80
  continue;
51
81
  const prev = lastKnownPr.get(ws.id);
@@ -163,7 +193,7 @@ export async function refreshPrSnapshot(workspaceId) {
163
193
  const ws = getWorkspace(workspaceId);
164
194
  if (!ws)
165
195
  throw new Error(`Workspace '${workspaceId}' not found`);
166
- const snap = await getPrStatusAsync(ws.projectPath, ws.workingBranch);
196
+ const snap = await getForgeProvider(resolveForge(ws.projectPath)).getPrStatus(ws.worktreePath, ws.workingBranch);
167
197
  if (snap === null) {
168
198
  lastKnownPr.delete(workspaceId);
169
199
  return null;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Bash equivalent of the built-in change-source-branch cherry-pick. Seeded
3
+ * into `global.changeSourceBranchScript` by settings migration v33; served to
4
+ * the client via `GET /api/settings/defaults` for the Settings "Reset to Kōbō
5
+ * default" button. Clearing the textarea disables the feature (empty script
6
+ * → menu item hidden).
7
+ */
8
+ export const DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT = `#!/usr/bin/env bash
9
+ # Kōbō default change-source-branch script — edit at will.
10
+ # Replaces the built-in cherry-pick when this field is non-empty.
11
+ #
12
+ # Env vars Kōbō exports for you:
13
+ # KOBO_NEW_BASE new source branch chosen in the dialog
14
+ # KOBO_OLD_BASE workspace's previous source branch
15
+ # KOBO_WORKING_BRANCH workspace's working branch
16
+ # KOBO_WORKTREE_PATH absolute path of the worktree (also cwd)
17
+ # KOBO_PROJECT_PATH absolute path of the main project repo
18
+ # KOBO_PROJECT_NAME project directory name (basename of KOBO_PROJECT_PATH)
19
+ # KOBO_WORKSPACE_ID Kōbō workspace id (stable across renames)
20
+ # KOBO_WORKSPACE_NAME workspace display name
21
+ # KOBO_FORGE resolved forge: github / gitlab / none
22
+ # KOBO_PR_NUMBER PR/MR number on the resolved forge (empty if none open)
23
+ set -euo pipefail
24
+
25
+ # Safety limit — refuse and ask for a manual rebase above this many commits.
26
+ MAX_PROPER_COMMITS=50
27
+
28
+ git fetch origin
29
+
30
+ # Commits proper to the working branch (in neither base).
31
+ COMMITS=$(git log --reverse --format=%H "$KOBO_WORKING_BRANCH" \\
32
+ --not "origin/$KOBO_NEW_BASE" "origin/$KOBO_OLD_BASE" || true)
33
+ COUNT=$(printf '%s\\n' "$COMMITS" | grep -c '^[0-9a-f]' || true)
34
+
35
+ # Backup branch so you can always recover: \`git reset --hard kobo-backup/…\`.
36
+ git branch "kobo-backup/\${KOBO_WORKING_BRANCH}-$(date +%s)" "$KOBO_WORKING_BRANCH"
37
+
38
+ if [ "$COUNT" -eq 0 ]; then
39
+ git reset --hard "origin/$KOBO_NEW_BASE"
40
+ elif [ "$COUNT" -gt "$MAX_PROPER_COMMITS" ]; then
41
+ echo "Too many proper commits ($COUNT > $MAX_PROPER_COMMITS) — rebase manually" >&2
42
+ exit 1
43
+ else
44
+ git reset --hard "origin/$KOBO_NEW_BASE"
45
+ printf '%s\\n' "$COMMITS" | xargs git cherry-pick
46
+ fi
47
+
48
+ # Re-target the PR / MR. Probe the CLI first so a missing tool degrades
49
+ # gracefully under \`set -e\`.
50
+ case "\${KOBO_FORGE:-none}" in
51
+ github)
52
+ if command -v gh >/dev/null 2>&1; then
53
+ gh pr edit --base "$KOBO_NEW_BASE" 2>/dev/null || true
54
+ else
55
+ echo "warn: 'gh' CLI not installed — skipping PR base update" >&2
56
+ fi
57
+ ;;
58
+ gitlab)
59
+ if command -v glab >/dev/null 2>&1; then
60
+ glab mr update --target-branch "$KOBO_NEW_BASE" 2>/dev/null || true
61
+ else
62
+ echo "warn: 'glab' CLI not installed — skipping MR base update" >&2
63
+ fi
64
+ ;;
65
+ *) : ;;
66
+ esac
67
+
68
+ # Force-push if the branch is tracked upstream.
69
+ if git rev-parse --abbrev-ref "@{upstream}" >/dev/null 2>&1; then
70
+ git push --force-with-lease origin "$KOBO_WORKING_BRANCH"
71
+ fi
72
+ `;
73
+ export function getSettingsDefaults() {
74
+ return {
75
+ changeSourceBranchScript: DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT,
76
+ };
77
+ }
@@ -8,6 +8,7 @@ import { getSettingsPath } from '../utils/paths.js';
8
8
  import { InvalidWorktreesPathError, resolveGlobalWorktreesRoot, sanitizeWorktreesPath, validateWorktreesPath, } from '../utils/worktree-paths.js';
9
9
  import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT } from './initial-prompt-template-service.js';
10
10
  import { DEFAULT_REVIEW_PROMPT_TEMPLATE } from './review-template-service.js';
11
+ import { DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT } from './settings-defaults.js';
11
12
  import { AGNOSTIC_PROMPTS } from './skill-suite-prompts.js';
12
13
  export const DEFAULT_GIT_CONVENTIONS = `# Git conventions
13
14
 
@@ -530,6 +531,31 @@ const settingsMigrations = [
530
531
  }
531
532
  },
532
533
  },
534
+ {
535
+ version: 32,
536
+ name: 'add-project-forge',
537
+ migrate: ({ projects }) => {
538
+ for (const p of projects) {
539
+ if (typeof p.forge !== 'string')
540
+ p.forge = 'auto';
541
+ }
542
+ },
543
+ },
544
+ {
545
+ version: 33,
546
+ name: 'add-change-source-branch-script',
547
+ migrate: ({ global, projects }) => {
548
+ // Seed global with the default so the feature is enabled out-of-the-box;
549
+ // projects stay empty (= inherit global).
550
+ if (typeof global.changeSourceBranchScript !== 'string') {
551
+ global.changeSourceBranchScript = DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT;
552
+ }
553
+ for (const p of projects) {
554
+ if (typeof p.changeSourceBranchScript !== 'string')
555
+ p.changeSourceBranchScript = '';
556
+ }
557
+ },
558
+ },
533
559
  ];
534
560
  /** Current settings schema version — always equals the highest migration version. */
535
561
  export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
@@ -569,6 +595,7 @@ function defaultSettings() {
569
595
  cleanupScriptMode: 'no-tasks',
570
596
  cleanupScriptOnlyOnChanges: false,
571
597
  archiveScript: '',
598
+ changeSourceBranchScript: DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT,
572
599
  editorCommand: '',
573
600
  browserNotifications: true,
574
601
  audioNotifications: true,
@@ -623,6 +650,7 @@ function defaultProjectSettings(projectPath) {
623
650
  cleanupScript: '',
624
651
  cleanupScriptMode: '',
625
652
  archiveScript: '',
653
+ changeSourceBranchScript: '',
626
654
  devServer: {
627
655
  startCommand: '',
628
656
  stopCommand: '',
@@ -636,6 +664,7 @@ function defaultProjectSettings(projectPath) {
636
664
  prompt: DEFAULT_FINALIZATION_PROMPT,
637
665
  },
638
666
  color: null,
667
+ forge: 'auto',
639
668
  };
640
669
  }
641
670
  function pickKnownKeys(data, allowedKeys) {
@@ -837,6 +866,7 @@ export function getEffectiveSettings(projectPath) {
837
866
  cleanupScriptMode: settings.global.cleanupScriptMode,
838
867
  cleanupScriptOnlyOnChanges: settings.global.cleanupScriptOnlyOnChanges,
839
868
  archiveScript: settings.global.archiveScript,
869
+ changeSourceBranchScript: settings.global.changeSourceBranchScript,
840
870
  notionStatusProperty: settings.global.notionStatusProperty,
841
871
  notionInProgressStatus: settings.global.notionInProgressStatus,
842
872
  };
@@ -862,6 +892,7 @@ export function getEffectiveSettings(projectPath) {
862
892
  cleanupScriptMode: (project.cleanupScriptMode || settings.global.cleanupScriptMode),
863
893
  cleanupScriptOnlyOnChanges: settings.global.cleanupScriptOnlyOnChanges,
864
894
  archiveScript: project.archiveScript || settings.global.archiveScript,
895
+ changeSourceBranchScript: project.changeSourceBranchScript || settings.global.changeSourceBranchScript,
865
896
  notionStatusProperty: settings.global.notionStatusProperty,
866
897
  notionInProgressStatus: settings.global.notionInProgressStatus,
867
898
  };
@@ -897,6 +928,7 @@ export function updateGlobalSettings(data) {
897
928
  'cleanupScriptMode',
898
929
  'cleanupScriptOnlyOnChanges',
899
930
  'archiveScript',
931
+ 'changeSourceBranchScript',
900
932
  'editorCommand',
901
933
  'browserNotifications',
902
934
  'audioNotifications',
@@ -1009,10 +1041,12 @@ export function upsertProject(projectPath, data) {
1009
1041
  'cleanupScript',
1010
1042
  'cleanupScriptMode',
1011
1043
  'archiveScript',
1044
+ 'changeSourceBranchScript',
1012
1045
  'devServer',
1013
1046
  'e2e',
1014
1047
  'finalization',
1015
1048
  'color',
1049
+ 'forge',
1016
1050
  ];
1017
1051
  const allowedDevServerKeys = ['startCommand', 'stopCommand'];
1018
1052
  const allowedE2eKeys = ['framework', 'skill', 'prompt'];
@@ -1,5 +1,5 @@
1
1
  import { execFile as execFileCb, execFileSync } from 'node:child_process';
2
- import { existsSync, readFileSync, rmSync } from 'node:fs';
2
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
5
  const execFileAsync = promisify(execFileCb);
@@ -139,6 +139,20 @@ export function fetchSourceBranch(repoPath, sourceBranch, remote = 'origin') {
139
139
  throw new Error(`Failed to fetch '${sourceBranch}' from '${remote}': ${message}`);
140
140
  }
141
141
  }
142
+ /**
143
+ * Fetch every branch from the remote (`git fetch <remote>` with no refspec).
144
+ * Throws if the fetch fails. Call this before computing branch divergence so
145
+ * all `origin/*` refs are current.
146
+ */
147
+ export function fetchAllBranches(repoPath, remote = 'origin') {
148
+ try {
149
+ git(repoPath, ['fetch', remote]);
150
+ }
151
+ catch (err) {
152
+ const message = err instanceof Error ? err.message : String(err);
153
+ throw new Error(`Failed to fetch from '${remote}': ${message}`);
154
+ }
155
+ }
142
156
  /** Pull the current branch from the remote using fast-forward only. */
143
157
  export function pullBranch(repoPath, branchName, remote = 'origin') {
144
158
  try {
@@ -149,8 +163,8 @@ export function pullBranch(repoPath, branchName, remote = 'origin') {
149
163
  throw new Error(`Failed to pull branch '${branchName}' from '${remote}': ${message}`);
150
164
  }
151
165
  }
152
- /** Thrown when a rebase or merge produces conflicts. Leaves the repo in the mid-operation state
153
- * so the caller can decide between abort and agent-assisted resolution. */
166
+ /** Thrown when a rebase, merge or cherry-pick produces conflicts. Leaves the repo in the
167
+ * mid-operation state so the caller can decide between abort and agent-assisted resolution. */
154
168
  export class GitConflictError extends Error {
155
169
  operation;
156
170
  files;
@@ -174,7 +188,7 @@ export function getConflictedFiles(repoPath) {
174
188
  return [];
175
189
  }
176
190
  }
177
- /** Detect whether a merge or rebase is currently in progress in the worktree. */
191
+ /** Detect whether a merge, rebase or cherry-pick is currently in progress in the worktree. */
178
192
  export function getOngoingGitOperation(repoPath) {
179
193
  try {
180
194
  const gitDir = git(repoPath, ['rev-parse', '--git-dir']);
@@ -183,6 +197,8 @@ export function getOngoingGitOperation(repoPath) {
183
197
  return 'merge';
184
198
  if (existsSync(join(dir, 'rebase-merge')) || existsSync(join(dir, 'rebase-apply')))
185
199
  return 'rebase';
200
+ if (existsSync(join(dir, 'CHERRY_PICK_HEAD')) || existsSync(join(dir, 'sequencer')))
201
+ return 'cherry-pick';
186
202
  return null;
187
203
  }
188
204
  catch {
@@ -230,7 +246,7 @@ export function mergeBranch(repoPath, baseBranch) {
230
246
  throw new Error(`Merge of 'origin/${baseBranch}' failed: ${message}`);
231
247
  }
232
248
  }
233
- /** Abort an in-progress merge or rebase. No-op if nothing is in progress. */
249
+ /** Abort an in-progress merge, rebase or cherry-pick. No-op if nothing is in progress. */
234
250
  export function abortOngoingGitOperation(repoPath) {
235
251
  const op = getOngoingGitOperation(repoPath);
236
252
  if (op === 'merge') {
@@ -239,6 +255,9 @@ export function abortOngoingGitOperation(repoPath) {
239
255
  else if (op === 'rebase') {
240
256
  git(repoPath, ['rebase', '--abort']);
241
257
  }
258
+ else if (op === 'cherry-pick') {
259
+ git(repoPath, ['cherry-pick', '--abort']);
260
+ }
242
261
  return op;
243
262
  }
244
263
  /** Try a git command with `base`, falling back to `origin/base` if the local ref is missing. */
@@ -290,6 +309,90 @@ export function getCommitsBehind(repoPath, base, head) {
290
309
  return 0;
291
310
  }
292
311
  }
312
+ /**
313
+ * List the commits that belong to `workingBranch` itself — reachable from it
314
+ * but present in neither `newBase` nor `oldBase`. This is the set to replay
315
+ * onto the new base. Returned oldest-first (ready for sequential cherry-pick).
316
+ *
317
+ * Both `origin/<base>` and the bare `<base>` are excluded when they exist, so
318
+ * the result is correct whether the caller fetched the base or not.
319
+ */
320
+ export function listProperCommits(repoPath, workingBranch, newBase, oldBase) {
321
+ const excludes = [];
322
+ for (const base of [newBase, oldBase]) {
323
+ for (const ref of [`origin/${base}`, base]) {
324
+ try {
325
+ git(repoPath, ['rev-parse', '--verify', '--quiet', ref]);
326
+ excludes.push(`^${ref}`);
327
+ }
328
+ catch {
329
+ // ref absent — skip
330
+ }
331
+ }
332
+ }
333
+ const output = git(repoPath, ['log', '--reverse', '--format=%H', workingBranch, ...excludes]);
334
+ return output
335
+ .split('\n')
336
+ .map((s) => s.trim())
337
+ .filter(Boolean);
338
+ }
339
+ /**
340
+ * Rebuild `workingBranch` on top of the new base by cherry-picking the given
341
+ * commits (oldest-first). Creates a backup branch at the current tip first and
342
+ * returns its name. On a cherry-pick conflict, leaves the operation in progress
343
+ * and throws `GitConflictError`.
344
+ *
345
+ * The base is resolved as `origin/<newBase>` when that ref exists, else the
346
+ * bare `<newBase>` (so it works both with a fetched remote and a local-only
347
+ * base). The caller must ensure the worktree is clean for the conflict path.
348
+ * An empty `commits` array performs the reset only — the "already aligned"
349
+ * fast path.
350
+ *
351
+ * IMPORTANT: this function resets the branch CURRENTLY checked out in
352
+ * `repoPath`. The caller (and the Kōbō worktree orchestrator) must ensure
353
+ * `workingBranch` is the active branch — do NOT add a `git checkout` here.
354
+ */
355
+ export function reconstructBranchOnto(repoPath, workingBranch, newBase, commits) {
356
+ const baseRef = resolveBase(repoPath, newBase);
357
+ const backupBranch = `kobo-backup/${workingBranch}-${Date.now()}`;
358
+ git(repoPath, ['branch', backupBranch, workingBranch]);
359
+ git(repoPath, ['reset', '--hard', baseRef]);
360
+ if (commits.length > 0) {
361
+ try {
362
+ git(repoPath, ['cherry-pick', ...commits]);
363
+ }
364
+ catch (err) {
365
+ const conflicted = getConflictedFiles(repoPath);
366
+ if (conflicted.length > 0 || getOngoingGitOperation(repoPath) === 'cherry-pick') {
367
+ throw new GitConflictError('cherry-pick', conflicted);
368
+ }
369
+ const message = err instanceof Error ? err.message : String(err);
370
+ throw new Error(`Cherry-pick onto '${newBase}' failed: ${message}`);
371
+ }
372
+ }
373
+ return backupBranch;
374
+ }
375
+ /** List `kobo-backup/<workingBranch>-<ts>` branches, newest timestamp first. */
376
+ export function listBackupBranches(repoPath, workingBranch) {
377
+ try {
378
+ const prefix = `kobo-backup/${workingBranch}-`;
379
+ const out = git(repoPath, ['branch', '--list', `${prefix}*`, '--format=%(refname:short)']);
380
+ return out
381
+ .split('\n')
382
+ .map((s) => s.trim())
383
+ .filter((b) => b.startsWith(prefix) && /^\d+$/.test(b.slice(prefix.length)))
384
+ .sort((a, b) => Number(b.slice(prefix.length)) - Number(a.slice(prefix.length)));
385
+ }
386
+ catch {
387
+ return [];
388
+ }
389
+ }
390
+ /** Abort any in-progress operation, then hard-reset `workingBranch` to a backup branch. */
391
+ export function restoreBranchFromBackup(repoPath, workingBranch, backupBranch) {
392
+ abortOngoingGitOperation(repoPath);
393
+ git(repoPath, ['checkout', '-q', workingBranch]);
394
+ git(repoPath, ['reset', '--hard', backupBranch]);
395
+ }
293
396
  /** Return structured diff shortstat between two refs (three-dot merge base). */
294
397
  export function getStructuredDiffStatsBetween(repoPath, base, head) {
295
398
  try {
@@ -397,105 +500,6 @@ export function listCommitsBehind(repoPath, sourceBranch, workingBranch, limit =
397
500
  }
398
501
  return commits;
399
502
  }
400
- /** Get the GitHub PR URL for a branch using `gh pr view`. Returns null if no PR exists. */
401
- export function getPrUrl(repoPath, branchName) {
402
- try {
403
- return (execFileSync('gh', ['pr', 'view', branchName, '--json', 'url', '--jq', '.url'], {
404
- cwd: repoPath,
405
- encoding: 'utf-8',
406
- }).trim() || null);
407
- }
408
- catch {
409
- return null;
410
- }
411
- }
412
- // NOTE: `reviewThreads` is intentionally NOT in this list.
413
- // As of `gh` CLI 2.92 (latest at time of writing) the `--json reviewThreads`
414
- // flag is rejected with `Unknown JSON field: "reviewThreads"` — there is no
415
- // stable `gh` version that exposes it via `pr view --json`. Until upstream
416
- // adds it, `unresolvedReviewThreadsCount` stays at 0 and the
417
- // "hide changes-requested badge when all threads are resolved" feature
418
- // degrades to "badge stays visible". To restore that feature later, either
419
- // (a) re-add the field here once `gh` supports it, or (b) make a separate
420
- // `gh api graphql` call for `pullRequest.reviewThreads.nodes`.
421
- const GH_PR_FIELDS = [
422
- 'number',
423
- 'title',
424
- 'url',
425
- 'state',
426
- 'baseRefName',
427
- 'reviewDecision',
428
- 'author',
429
- 'assignees',
430
- 'labels',
431
- 'latestReviews',
432
- 'reviewRequests',
433
- 'statusCheckRollup',
434
- 'updatedAt',
435
- ].join(',');
436
- function mapGhPrToSnapshot(raw) {
437
- const reviewers = [];
438
- const seen = new Set();
439
- for (const r of raw.latestReviews ?? []) {
440
- const login = r.author?.login;
441
- if (!login || seen.has(login))
442
- continue;
443
- seen.add(login);
444
- reviewers.push({ login, state: r.state ?? 'COMMENTED' });
445
- }
446
- for (const r of raw.reviewRequests ?? []) {
447
- if (!r.login || seen.has(r.login))
448
- continue;
449
- seen.add(r.login);
450
- reviewers.push({ login: r.login, state: 'PENDING' });
451
- }
452
- const checks = (raw.statusCheckRollup ?? []).map((c) => ({
453
- name: c.name,
454
- conclusion: c.conclusion ?? null,
455
- status: c.status,
456
- detailsUrl: c.detailsUrl ?? null,
457
- }));
458
- let rollup = null;
459
- if (checks.length > 0) {
460
- if (checks.some((c) => c.conclusion === 'FAILURE'))
461
- rollup = 'FAILURE';
462
- else if (checks.some((c) => c.status !== 'COMPLETED'))
463
- rollup = 'PENDING';
464
- else
465
- rollup = 'SUCCESS';
466
- }
467
- const unresolvedReviewThreadsCount = (raw.reviewThreads ?? []).reduce((acc, t) => acc + (t.isResolved ? 0 : 1), 0);
468
- return {
469
- number: raw.number,
470
- title: raw.title,
471
- url: raw.url,
472
- state: raw.state,
473
- base: raw.baseRefName ?? '',
474
- reviewDecision: raw.reviewDecision ?? null,
475
- author: { login: raw.author?.login ?? '' },
476
- assignees: (raw.assignees ?? []).map((a) => ({ login: a.login })),
477
- reviewers,
478
- labels: (raw.labels ?? []).map((l) => ({ name: l.name, color: l.color })),
479
- ci: { rollup, checks },
480
- updatedAt: raw.updatedAt ?? '',
481
- unresolvedReviewThreadsCount,
482
- };
483
- }
484
- /** Get a rich snapshot of the PR for a branch. Returns null if no PR exists. */
485
- export function getPrStatus(repoPath, branchName) {
486
- try {
487
- const raw = execFileSync('gh', ['pr', 'view', branchName, '--json', GH_PR_FIELDS], {
488
- cwd: repoPath,
489
- encoding: 'utf-8',
490
- }).trim();
491
- if (!raw)
492
- return null;
493
- return mapGhPrToSnapshot(JSON.parse(raw));
494
- }
495
- catch {
496
- return null;
497
- }
498
- }
499
503
  /**
500
504
  * Rename a branch in-place (`git branch -m <old> <new>`). Must be run inside
501
505
  * the worktree (or any directory tracking the repo) — the new name replaces
@@ -759,6 +763,10 @@ export function getFileContent(repoPath, filePath) {
759
763
  return null;
760
764
  }
761
765
  }
766
+ /** Write content to an absolute path inside a worktree. Caller validates the path. */
767
+ export function writeFileInWorktree(absPath, content) {
768
+ writeFileSync(absPath, content, 'utf-8');
769
+ }
762
770
  /** Parse `git status --porcelain` into counts of staged, modified, and untracked files. */
763
771
  export function getWorkingTreeStatus(repoPath) {
764
772
  try {
@@ -846,36 +854,7 @@ export function getWorkingTreeDiffStats(repoPath) {
846
854
  }
847
855
  }
848
856
  // ── Async versions ───────────────────────────────────────────────────────────
849
- // Non-blocking alternatives for hot paths (pr-watcher, route handlers).
850
- /** Async version of getPrUrl. Returns null if no PR exists. */
851
- export async function getPrUrlAsync(repoPath, branchName) {
852
- try {
853
- const { stdout } = await execFileAsync('gh', ['pr', 'view', branchName, '--json', 'url', '--jq', '.url'], {
854
- cwd: repoPath,
855
- encoding: 'utf-8',
856
- });
857
- return stdout.trim() || null;
858
- }
859
- catch {
860
- return null;
861
- }
862
- }
863
- /** Async version of getPrStatus. Returns null if no PR exists. */
864
- export async function getPrStatusAsync(repoPath, branchName) {
865
- try {
866
- const { stdout } = await execFileAsync('gh', ['pr', 'view', branchName, '--json', GH_PR_FIELDS], {
867
- cwd: repoPath,
868
- encoding: 'utf-8',
869
- });
870
- const raw = stdout.trim();
871
- if (!raw)
872
- return null;
873
- return mapGhPrToSnapshot(JSON.parse(raw));
874
- }
875
- catch {
876
- return null;
877
- }
878
- }
857
+ // Non-blocking alternatives for hot paths (route handlers).
879
858
  /**
880
859
  * Async version of `getUnpushedCount`. Same `origin/<workingBranch>` semantic:
881
860
  * returns `-1` when the remote ref does not exist (never pushed), `0` when
@@ -917,3 +896,11 @@ export async function fetchSourceBranchAsync(repoPath, branch, remote = 'origin'
917
896
  console.warn(`[git-ops] fetchSourceBranchAsync(${remote}/${branch}) failed: ${msg}`);
918
897
  }
919
898
  }
899
+ /** Stash all changes (including untracked). */
900
+ export function stashPush(repoPath, label) {
901
+ git(repoPath, ['stash', 'push', '--include-untracked', '-m', label]);
902
+ }
903
+ /** Pop the most recent stash entry. */
904
+ export function stashPop(repoPath) {
905
+ git(repoPath, ['stash', 'pop']);
906
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "1.7.15",
3
+ "version": "1.7.17",
4
4
  "description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -0,0 +1,8 @@
1
+ import{D as e,G as t,I as n,N as r,P as i,R as a,St as o,W as s,_ as c,at as l,d as u,et as d,f,g as p,l as m,p as h,r as g,u as _,xt as v,y,yt as b}from"./runtime-core.esm-bundler-D_RRiKBh.js";import{U as x,l as S,t as C}from"./QIcon-BmEX2rXO.js";import{c as w,s as T}from"./notifications-Hq-6rEYv.js";import{t as E}from"./QBtn-CoU-UC_j.js";import{n as D}from"./vue-i18n-Cq-KgjJC.js";import{_ as O,c as k,m as A,u as j}from"./index-DMUFfCIq.js";import{t as M}from"./QSpinnerDots-DspFKwCZ.js";import{t as N}from"./QTooltip-BC7PnZJ1.js";import{t as ee}from"./QExpansionItem-CgJQdznK.js";import{t as te}from"./QScrollArea-_Ji1cgqL.js";import{i as ne,n as re,t as P}from"./render-chat-markdown-BywKNkXe.js";import{t as F}from"./documents-B3nitIYF.js";import{t as I}from"./_plugin-vue_export-helper-Cj6tcsj6.js";function ie(e,t,n=!0){let r=[],i=new Map,a=new Map;for(let n=0;n<e.length;n++){let o=e[n],s=t?.[n];switch(o.kind){case`message:text`:{let e=i.get(o.messageId);if(e)e.text+=o.text,e.streaming=o.streaming;else{let e={type:`text`,messageId:o.messageId,text:o.text,streaming:o.streaming,ts:s};i.set(o.messageId,e),r.push(e)}break}case`message:end`:{let e=i.get(o.messageId);e&&(e.streaming=!1);break}case`message:thinking`:r.push({type:`thinking`,messageId:o.messageId,text:o.text,ts:s});break;case`tool:call`:{let e={type:`tool`,toolCallId:o.toolCallId,name:o.name,input:o.input,ts:s};a.set(o.toolCallId,e),r.push(e);break}case`tool:result`:{let e=a.get(o.toolCallId);e&&(e.result={output:o.output,isError:o.isError});break}case`session:started`:r.push({type:`session`,kind:`started`,detail:{engineSessionId:o.engineSessionId,model:o.model},ts:s});break;case`session:ended`:r.push({type:`session`,kind:`ended`,detail:{reason:o.reason,exitCode:o.exitCode},ts:s});break;case`session:compacted`:r.push({type:`session`,kind:`compacted`,ts:s});break;case`session:brainstorm-complete`:case`session:user-input-requested`:case`message:raw`:case`skills:discovered`:case`usage`:case`rate_limit`:case`subagent:progress`:case`error`:break;default:}}let o=null;for(let e of r)e.type===`text`&&e.streaming&&(o&&(o.streaming=!1),o=e);return o&&!n&&(o.streaming=!1),r}function ae(e,t){if(t.length===0)return e;let n=t.map(e=>({type:`user`,content:e.content,sender:e.sender,ts:e.ts})),r=[...e,...n];r.sort((e,t)=>{let n=e.ts??``,r=t.ts??``;return n===r?0:n?r?n<r?-1:1:-1:1});let i;for(let e of r)e.type===`user`&&e.sender!==`system-prompt`&&e.ts&&(!i||e.ts>i)&&(i=e.ts);if(i)for(let e of r)e.type===`text`&&e.streaming&&(!e.ts||e.ts<i)&&(e.streaming=!1);return r}var L=new Set([`setup`,`cleanup`,`archive`]);function R(e){switch(e.type){case`user`:return e.sender===`system-prompt`?`system-prompt`:L.has(e.sender)?`script`:`user`;case`session`:return`session`;default:return`agent`}}function oe(e){let t=[],n=null,r=null;for(let i of e){let e=R(i),a=e===`session`||e===`system-prompt`,o=e===`script`&&i.type===`user`?`script:${i.sender}`:e;!n||r!==o||a?(n={speaker:e,ts:i.ts,items:[i]},r=o,t.push(n),a&&(n=null)):n.items.push(i)}return t}var z={class:`text-caption text-grey-6`},B=y({__name:`SessionEventItem`,props:{item:{}},setup(e){let t=e,r=m(()=>{switch(t.item.kind){case`started`:return`session.started`;case`ended`:return`session.ended`;case`compacted`:return`session.compacted`;default:return`session.started`}});return(e,t)=>(n(),h(`span`,z,o(e.$t(r.value)),1))}});function se(e,t){if(t.length===0||e.length===0)return e;let n=[...t].sort((e,t)=>t.length-e.length),r=new DOMParser().parseFromString(`<div>${e}</div>`,`text/html`),i=r.body.firstChild;if(!i)return e;function a(e){if(e.nodeType===Node.TEXT_NODE){V(e,n,r);return}if(e.nodeName===`A`)return;let t=Array.from(e.childNodes);for(let e of t)a(e)}return a(i),i.innerHTML}function V(e,t,n){let r=e.textContent??``;if(!t.some(e=>r.includes(e)))return;let i=n.createDocumentFragment(),a=0;for(;a<r.length;){let e=H(r,a,t);if(!e){i.appendChild(n.createTextNode(r.slice(a)));break}e.index>a&&i.appendChild(n.createTextNode(r.slice(a,e.index)));let o=n.createElement(`a`);o.className=`document-link`,o.setAttribute(`data-document-path`,e.path),o.setAttribute(`href`,`#`),o.textContent=e.path,i.appendChild(o),a=e.index+e.path.length}e.parentNode?.replaceChild(i,e)}function H(e,t,n){let r=null;for(let i of n){let n=e.indexOf(i,t);n<0||(!r||n<r.index||n===r.index&&i.length>r.path.length)&&(r={index:n,path:i})}return r}var U=[`innerHTML`],ce=I(y({__name:`TextMessageItem`,props:{item:{}},setup(e){let t=e,r=F(),i=k(),a=m(()=>{let e=i.selectedWorkspaceId;return e?r.documentsFor(e).map(e=>e.path):[]}),o=m(()=>re(se(ne.parse(t.item.text,{async:!1,breaks:!0,gfm:!0}),a.value),{addAttr:[`data-document-path`]}));function s(e){let t=e.target?.closest(`.document-link`);if(!t)return;e.preventDefault();let n=t.getAttribute(`data-document-path`),a=i.selectedWorkspaceId;!n||!a||r.openDocumentByPath(a,n)}return(t,r)=>(n(),h(`div`,{class:`markdown-message`,onClick:s},[_(`div`,{innerHTML:o.value},null,8,U),e.item.streaming?(n(),u(S,{key:0,size:`xs`,class:`q-ml-xs`})):f(``,!0)]))}}),[[`__scopeId`,`data-v-1b7bd8ca`]]),W={key:0,class:`text-caption text-grey-5`,style:{"font-style":`italic`}},G=[`innerHTML`],K={key:1,style:{"white-space":`pre-wrap`}},le=I(y({__name:`ThinkingItem`,props:{item:{}},setup(e){let r=e,i=m(()=>r.item.text.trim().slice(0,100)),a=m(()=>r.item.text.trim().length>0),s=m(()=>r.item.text.trim().length>100),c=m(()=>P(r.item.text));return(r,l)=>a.value?(n(),h(`div`,W,[s.value?(n(),u(ee,{key:0,dense:``,"dense-toggle":``,label:i.value,"header-class":`text-grey-5 text-caption`,style:{"font-style":`italic`}},{default:t(()=>[_(`div`,{class:`q-py-xs markdown-thinking`,innerHTML:c.value},null,8,G)]),_:1},8,[`label`])):(n(),h(`span`,K,o(e.item.text),1))])):f(``,!0)}}),[[`__scopeId`,`data-v-7f45ed94`]]);function ue(e,t){let n=e.split(`
2
+ `),r=t.split(`
3
+ `),i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array(a+1).fill(0));for(let e=i-1;e>=0;e--)for(let t=a-1;t>=0;t--)n[e]===r[t]?o[e][t]=o[e+1][t+1]+1:o[e][t]=Math.max(o[e+1][t],o[e][t+1]);let s=[],c=0,l=0;for(;c<i&&l<a;)n[c]===r[l]?(s.push({type:`context`,content:n[c]}),c++,l++):o[c+1][l]>=o[c][l+1]?(s.push({type:`del`,content:n[c]}),c++):(s.push({type:`add`,content:r[l]}),l++);for(;c<i;)s.push({type:`del`,content:n[c++]});for(;l<a;)s.push({type:`add`,content:r[l++]});return s}function de(e){let t=e.split(`
4
+ `),n=[];for(let e of t)e.startsWith(`@@`)||e.startsWith(`+++`)||e.startsWith(`---`)||(e.startsWith(`+`)?n.push({type:`add`,content:e.slice(1)}):e.startsWith(`-`)?n.push({type:`del`,content:e.slice(1)}):e.startsWith(` `)?n.push({type:`context`,content:e.slice(1)}):e.length>0&&n.push({type:`context`,content:e}));return n}function fe(e,t){if(!t||typeof t!=`object`)return null;let n=t;if(e===`Edit`){let e=n.file_path;if(!e)return null;let t=n.old_string??``,r=n.new_string??``,i=typeof n.diff==`string`?n.diff:``;if(!t&&!r&&i.length>0){let t=de(i);return{toolName:`Edit`,filePath:e,additions:t.filter(e=>e.type===`add`).length,deletions:t.filter(e=>e.type===`del`).length,diffLines:t}}return{toolName:`Edit`,filePath:e,oldString:t,newString:r,replaceAll:n.replace_all??!1,additions:r?r.split(`
5
+ `).length:0,deletions:t?t.split(`
6
+ `).length:0}}if(e===`Write`){let e=n.file_path;if(!e)return null;let t=n.content??``;return{toolName:`Write`,filePath:e,content:t,additions:t?t.split(`
7
+ `).length:0,deletions:0}}if(e===`Bash`){let e=(n.command??``).match(/^\s*rm\s+(?:-[a-zA-Z]*\s+)*(.+)/);if(e)return{toolName:`Bash:rm`,filePath:e[1].trim().replace(/["']/g,``),additions:0,deletions:1}}return null}function q(e,t){if(!e||!t?.projectPath)return e;let n=t.worktreePath;if(n){let t=J(e,n);if(t!==e)return t}let r=J(e,`${t.projectPath}/${w}/${t.workingBranch}`);return r===e?J(e,t.projectPath):r}function J(e,t){if(!t)return e;let n=pe(t);return n?e.replace(RegExp(`${n}[\\\\/]+`,`g`),``).replace(RegExp(`${n}(?=\\s|$|["'\`])`,`g`),`.`):e}function pe(e){let t=e.replace(/[\\/]+$/,``);return t?`${/^[\\/]+/.test(t)?`[\\\\/]+`:``}${t.split(/[\\/]+/).filter(Boolean).map(me).join(`[\\\\/]+`)}`:``}function me(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}var Y={class:`tool-name`},X=[`title`],he={key:0,class:`tool-stat-add`},ge={key:1,class:`tool-stat-del`},Z={class:`diff-sign`},Q={class:`tool-name`},$=[`title`],_e=I(y({__name:`ToolCallItem`,props:{item:{}},setup(e){let t=e,r=d(!1),i=k(),l=m(()=>fe(t.item.name,t.item.input)),v=m(()=>l.value?q(l.value.filePath,i.selectedWorkspace):``),y={Bash:`terminal`,Read:`description`,Edit:`edit`,Write:`edit_note`,MultiEdit:`edit`,Glob:`folder_open`,Grep:`manage_search`,LS:`list`,Skill:`auto_awesome`,Task:`hub`,Agent:`hub`,TodoWrite:`checklist`,TodoRead:`checklist`,ToolSearch:`search`,WebFetch:`public`,WebSearch:`travel_explore`,NotebookRead:`book`,NotebookEdit:`edit_note`,SendMessage:`send`,ExitPlanMode:`check_circle_outline`,KillShell:`stop_circle`,BashOutput:`terminal`},S=m(()=>y[t.item.name]??`build`),w=m(()=>{if(l.value)return``;let e=t.item.input,n=T(e);return n?q(n,i.selectedWorkspace):``});function T(e){if(!e||typeof e!=`object`)return typeof e==`string`?e:``;let t=e;for(let e of[`file_path`,`path`,`command`,`pattern`,`query`,`url`,`skill`,`description`,`subject`,`prompt`]){let n=t[e];if(typeof n==`string`&&n.length>0)return n}for(let e of Object.values(t))if(typeof e==`string`&&e.length>0)return e;return``}let E=m(()=>{let e=l.value;return e?e.diffLines?e.diffLines:e.toolName===`Edit`&&e.oldString!==void 0&&e.newString!==void 0?ue(e.oldString,e.newString):e.toolName===`Write`&&e.content!==void 0?e.content.split(`
8
+ `).map(e=>({type:`add`,content:e})):e.toolName===`Bash:rm`?[{type:`del`,content:`File deleted`}]:null:null}),D=m(()=>{let e=t.item.result;if(!e)return``;if(typeof e.output==`string`)return e.output;try{return JSON.stringify(e.output)}catch{return String(e.output)}}),O=new Set([`Read`]),A=m(()=>!!(t.item.result&&D.value)&&(!O.has(t.item.name)||t.item.result?.isError===!0));function j(){r.value=!r.value}return s(()=>t.item.result?.isError===!0,e=>{e&&(r.value=!0)},{immediate:!0}),(t,i)=>l.value?(n(),h(`div`,{key:0,class:b([`tool-row`,{"tool-row-expanded":r.value}])},[_(`div`,{class:`tool-header`,onClick:j},[c(C,{name:S.value,size:`14px`,class:`tool-icon`},null,8,[`name`]),_(`span`,Y,o(l.value.toolName===`Bash:rm`?`Bash`:l.value.toolName),1),_(`span`,{class:`tool-path`,title:l.value.filePath},o(v.value),9,X),l.value.additions>0?(n(),h(`span`,he,`+`+o(l.value.additions),1)):f(``,!0),l.value.deletions>0?(n(),h(`span`,ge,`-`+o(l.value.deletions),1)):f(``,!0),e.item.result?.isError?(n(),u(C,{key:2,name:`error_outline`,color:`negative`,size:`xs`,class:`q-ml-xs`})):e.item.result?(n(),u(C,{key:3,name:`check`,color:`positive`,size:`xs`,class:`q-ml-xs`})):f(``,!0),c(C,{name:r.value?`expand_less`:`expand_more`,size:`xs`,class:`q-ml-auto text-grey-6`},null,8,[`name`])]),r.value&&E.value?(n(),h(`div`,{key:0,class:`tool-diff`,onClick:i[0]||=x(()=>{},[`stop`])},[(n(!0),h(g,null,a(E.value,(e,t)=>(n(),h(`div`,{key:t,class:b([`diff-line`,{"diff-del":e.type===`del`,"diff-add":e.type===`add`,"diff-context":e.type===`context`}])},[_(`span`,Z,o(e.type===`del`?`-`:e.type===`add`?`+`:` `),1),p(o(e.content),1)],2))),128))])):f(``,!0)],2)):(n(),h(`div`,{key:1,class:b([`tool-row tool-row-generic`,{"tool-row-expanded":r.value,"tool-row--toggleable":A.value}])},[_(`div`,{class:`tool-header`,onClick:i[1]||=e=>A.value&&j()},[c(C,{name:S.value,size:`14px`,class:`tool-icon`},null,8,[`name`]),_(`span`,Q,o(e.item.name),1),w.value?(n(),h(`span`,{key:0,class:`tool-arg`,title:T(e.item.input)||w.value},o(w.value),9,$)):f(``,!0),e.item.result?.isError?(n(),u(C,{key:1,name:`error_outline`,color:`negative`,size:`xs`,class:`q-ml-auto`})):e.item.result?(n(),u(C,{key:2,name:`check`,color:`positive`,size:`xs`,class:`q-ml-auto`})):f(``,!0),A.value?(n(),u(C,{key:3,name:r.value?`expand_less`:`expand_more`,size:`xs`,class:`q-ml-xs text-grey-6`},null,8,[`name`])):f(``,!0)]),r.value&&A.value?(n(),h(`div`,{key:0,class:`tool-output`,onClick:i[2]||=x(()=>{},[`stop`])},o(D.value),1)):f(``,!0)],2))}}),[[`__scopeId`,`data-v-1702f1be`]]);function ve(e,t){return t?e.replace(/\[image:\s+([^\]]+)\]/g,(e,n)=>{let r=String(n).trim();return/^(\.ai\/images\/|images\/)/.test(r)?`![${r}](${`/api/workspaces/${encodeURIComponent(t)}/images/file?path=${encodeURIComponent(r)}`})`:e}):e}var ye=[`innerHTML`],be=[`innerHTML`],xe=[`src`],Se=I(y({__name:`UserMessageItem`,props:{item:{}},setup(e){let r=e,i=k(),a=m(()=>r.item.sender===`system-prompt`),o=m(()=>P(ve(r.item.content,i.selectedWorkspaceId??``))),s=d(null),l=d(!1);function p(e){let t=e.target;if(t?.tagName!==`IMG`)return;let n=t;n.src&&(s.value=n.src,l.value=!0)}return(e,r)=>(n(),h(g,null,[a.value?(n(),u(ee,{key:0,dense:``,"dense-toggle":``,label:e.$t(`chat.systemPrompt`),"header-class":`text-grey-5 text-caption`},{default:t(()=>[_(`div`,{class:`q-py-xs markdown-user-prompt`,innerHTML:o.value},null,8,ye)]),_:1},8,[`label`])):(n(),h(`div`,{key:1,class:`markdown-message`,onClick:p},[_(`div`,{innerHTML:o.value},null,8,be)])),c(O,{modelValue:l.value,"onUpdate:modelValue":r[1]||=e=>l.value=e},{default:t(()=>[s.value?(n(),h(`img`,{key:0,src:s.value,alt:``,class:`image-lightbox-img`,onClick:r[0]||=e=>l.value=!1},null,8,xe)):f(``,!0)]),_:1},8,[`modelValue`])],64))}}),[[`__scopeId`,`data-v-f34be4c5`]]),Ce={class:`turn-header`},we={key:0,class:`turn-time`},Te={class:`turn-time turn-time-updated`},Ee={key:2,class:`turn-actions`},De={class:`turn-body`},Oe={key:0,class:`turn-scroll-top`},ke=I(y({__name:`TurnCard`,props:{turn:{}},emits:[`scrollTo`],setup(e,{emit:r}){let i=e,s=r,{t:y}=D(),x=d(null);function S(){let e=x.value;if(!e)return;let t=e.closest(`.q-scrollarea`)?.querySelector(`.q-scrollarea__content`);if(!t){e.scrollIntoView({behavior:`smooth`,block:`start`});return}let n=e.getBoundingClientRect().top-t.getBoundingClientRect().top;s(`scrollTo`,Math.max(0,n-8))}let w=m(()=>{switch(i.turn.speaker){case`user`:return{label:y(`chat.you`),accent:`#ce93d8`,badgeClass:`turn-badge-user`};case`agent`:return{label:y(`chat.agent`),accent:`#7986cb`,badgeClass:`turn-badge-agent`};case`system-prompt`:return{label:y(`chat.systemPrompt`),accent:`#757575`,badgeClass:`turn-badge-system`};case`session`:return{label:y(`chat.session`),accent:`#616161`,badgeClass:`turn-badge-session`};case`script`:{let e=i.turn.items[0],t=e?.type===`user`?e.sender:``;return{label:y(t===`archive`?`chat.archiveScript`:t===`setup`?`chat.setupScript`:`chat.cleanupScript`),accent:`#4db6ac`,badgeClass:`turn-badge-script`}}}});function T(e,t=!1){if(!e)return``;let n=new Date(e);return Number.isNaN(n.getTime())?``:n.toLocaleTimeString(void 0,t?{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}:{hour:`2-digit`,minute:`2-digit`})}let O=m(()=>T(i.turn.ts)),k=m(()=>{let e=i.turn.items;if(e.length===0)return null;for(let t=e.length-1;t>=0;t--){let n=e[t].ts;if(n)return n}return null}),A=m(()=>{let e=i.turn.ts,t=k.value;if(!t||!e||t===e)return``;let n=new Date(e).getTime(),r=new Date(t).getTime();return Number.isNaN(n)||Number.isNaN(r)||r<=n?``:T(t,r-n<6e4)}),j=m(()=>A.value!==``),M=m(()=>i.turn.items.filter(e=>e.type===`tool`).length);return(r,i)=>(n(),h(`div`,{ref_key:`cardEl`,ref:x,class:b([`turn-card`,{"turn-card--user":e.turn.speaker===`user`}]),style:v({"--turn-accent":w.value.accent})},[_(`div`,Ce,[_(`span`,{class:b([`turn-badge`,w.value.badgeClass])},o(w.value.label),3),O.value?(n(),h(`span`,we,o(O.value),1)):f(``,!0),j.value?(n(),h(g,{key:1},[c(C,{name:`arrow_forward`,size:`10px`,color:`grey-7`,class:`turn-time-arrow`}),_(`span`,Te,[p(o(A.value)+` `,1),c(N,null,{default:t(()=>[p(o(l(y)(`chat.lastUpdatedAt`,{time:A.value})),1)]),_:1})])],64)):f(``,!0),M.value>0?(n(),h(`span`,Ee,` · `+o(l(y)(`chat.nActions`,{n:M.value})),1)):f(``,!0)]),_(`div`,De,[(n(!0),h(g,null,a(e.turn.items,(e,t)=>(n(),h(g,{key:t},[e.type===`text`?(n(),u(ce,{key:0,item:e},null,8,[`item`])):e.type===`thinking`?(n(),u(le,{key:1,item:e},null,8,[`item`])):e.type===`tool`?(n(),u(_e,{key:2,item:e},null,8,[`item`])):e.type===`user`?(n(),u(Se,{key:3,item:e},null,8,[`item`])):e.type===`session`?(n(),u(B,{key:4,item:e},null,8,[`item`])):f(``,!0)],64))),128))]),e.turn.items.length>4?(n(),h(`div`,Oe,[c(E,{flat:``,round:``,dense:``,size:`xs`,icon:`arrow_upward`,color:`grey-6`,class:`turn-scroll-top-btn`,onClick:S},{default:t(()=>[c(N,null,{default:t(()=>[p(o(l(y)(`chat.scrollToTurnTop`)),1)]),_:1})]),_:1})])):f(``,!0)],6))}}),[[`__scopeId`,`data-v-ce5d132b`]]),Ae={key:0,class:`activity-feed-switching`},je={key:1,class:`activity-feed-wrap`},Me={key:0,class:`text-center q-py-sm text-caption text-grey-6`},Ne={class:`q-pa-md`},Pe={key:1,class:`q-px-md q-pb-md`},Fe={class:`activity-feed-nav-cluster`},Ie=60,Le=200,Re=200,ze=400,Be=200,Ve=180,He=I(y({__name:`ActivityFeed`,props:{workspaceId:{}},setup(l){let v=l,y=A(),b=T(),x=k(),C=m(()=>x.selectedSessionId),w=m(()=>x.sessions.find(e=>e.id===C.value)?.engineSessionId??null),D=m(()=>{let e=x.sessions;return e.length===0?!1:C.value===e[e.length-1].id});function O(e){return C.value?e?e===C.value||e===w.value:D.value:!0}let N=m(()=>(x.activityFeeds[v.workspaceId]??[]).filter(e=>e.type===`text`&&typeof e.content==`string`&&O(e.sessionId)).map(e=>({content:e.content,sender:e.meta?.sender??`user`,ts:e.timestamp,sessionId:e.sessionId}))),ne=m(()=>j(x.workspaces.find(e=>e.id===v.workspaceId)?.status)),re=m(()=>{let e=y.eventsFor(v.workspaceId),t=y.timestampsFor(v.workspaceId),n=y.sessionIdsFor(v.workspaceId),r=[],i=[];for(let a=0;a<e.length;a++)O(n[a])&&(r.push(e[a]),i.push(t[a]));let a=ae(ie(r,i,ne.value),N.value);return oe(b.showVerboseSystemMessages?a:a.filter(e=>e.type!==`session`))}),P=m(()=>b.showVerboseSystemMessages?y.eventsFor(v.workspaceId).filter(e=>e.kind===`message:raw`).map(e=>e.content):[]),F=d(null),I=d(!0),L=d(!1),R=!1,z=d(!0),B=d(new Map);function se(e){I.value=e.verticalSize-e.verticalPosition-e.verticalContainerSize<=Ie,R&&e.verticalPosition<=Le&&!L.value&&H()&&W()}function V(e,t){return`${e}:${t}`}function H(){let e=C.value;return e?B.value.get(V(v.workspaceId,e))??!0:y.hasMoreOlderFor(v.workspaceId)}function U(e,t,n){B.value.set(V(e,t),n)}function ce(e){if(!C.value)return y.oldestIdFor(e);let t=y.eventIdsFor(e),n=y.sessionIdsFor(e);for(let e=0;e<t.length;e++){if(!O(n[e]))continue;let r=t[e];if(r)return r}}async function W(){let t=v.workspaceId,n=C.value,r=ce(t);if(!r)return;L.value=!0;let i=Date.now();try{let i=F.value;await e();let a=i?.getScroll().verticalSize??0,o=i?.getScroll().verticalPosition??0,s=new URLSearchParams({before:r,limit:`200`});n&&s.set(`session`,n);let c=fetch(`/api/workspaces/${t}/events?${s.toString()}`),l=new Promise(e=>setTimeout(e,Re)),[u]=await Promise.all([c,l]);if(!u.ok){n?U(t,n,!1):y.prepend(t,[],[],{oldestId:r,hasMoreOlder:!1});return}let d=await u.json(),f=d.events??[],p=f.filter(e=>e.type===`agent:event`&&e.workspaceId===t),m=f.filter(e=>e.type===`user:message`&&e.workspaceId===t),h=p.map(e=>e.payload),g=p.map(e=>e.createdAt),_=p.map(e=>e.sessionId??null),v=p.map(e=>e.id),b=f.length>0?f[0].id:r;n&&U(t,n,d.hasMore),y.prepend(t,h,g,{oldestId:b,hasMoreOlder:n?y.hasMoreOlderFor(t):d.hasMore,sessionIds:_,eventIds:v});for(let e of m){let n=e.payload;typeof n.content==`string`&&x.addActivityItem(t,{id:e.id,type:`text`,content:n.content,timestamp:e.createdAt,sessionId:e.sessionId??void 0,meta:{sender:n.sender??`user`}})}if(await e(),i){let e=i.getScroll().verticalSize-a;if(e>0){let t=Math.max(o+e,Le+50);i.setScrollPosition(`vertical`,t,0)}}}catch(e){console.error(`[ActivityFeed] failed to load older events:`,e)}finally{let e=Date.now()-i,t=Math.max(0,Re-e);await new Promise(e=>setTimeout(e,t+ze)),L.value=!1}}async function G(t=0){await e();let n=F.value;if(!n)return;let r=n.getScroll();n.setScrollPosition(`vertical`,r.verticalSize,t)}let K=null,le=0;function ue(){if(K!=null)return;let e=performance.now()-le<Ve;K=requestAnimationFrame(()=>{K=null,le=performance.now(),G(e?0:180)})}function de(e){let t=F.value;t&&t.setScrollPosition(`vertical`,Math.max(0,e),250)}let fe=d([]),q=d(null);function J(){let e=re.value,t=fe.value,n=[];if(t.length===e.length){for(let r=0;r<e.length;r++){if(e[r].speaker!==`user`)continue;let i=t[r]?.$el;i&&n.push(i)}if(n.length>0)return n}let r=q.value?.parentElement;if(r){let e=r.querySelectorAll(`.turn-card--user`);for(let t of e)n.push(t)}return n}function pe(){let e=F.value;if(!e)return null;let t=q.value;if(!t)return null;let n=e.getScroll().verticalPosition,r=t.getBoundingClientRect().top,i=null;for(let e of J()){let t=e.getBoundingClientRect().top-r;if(t<n-40)i=t;else break}return i}async function me(){let t=F.value;if(!t)return;let n=pe();if(n===null)for(let t=0;t<15&&H();t++){for(;L.value;)await new Promise(e=>setTimeout(e,50));if(await W(),await e(),n=pe(),n!==null)break}n!==null&&t.setScrollPosition(`vertical`,Math.max(0,n-12),250)}async function Y(){R=!1,await e(),await G(0),requestAnimationFrame(()=>{requestAnimationFrame(()=>{R=!0})})}let X=m(()=>{let e=y.sessionIdsFor(v.workspaceId);if(!C.value)return e.length;let t=0;for(let n of e)O(n)&&t++;return t}),he=m(()=>y.eventsFor(v.workspaceId).length);async function ge(){z.value=!0;let e=Date.now();await new Promise(e=>setTimeout(e,Be));let t=e+5e3;for(;he.value===0&&Date.now()<t;)await new Promise(e=>setTimeout(e,50));z.value=!1}s(z,async e=>{!e&&X.value>0&&await Y(),!e&&X.value===0&&C.value&&$()}),r(()=>{ge(),X.value>0&&Y(),C.value&&$()});let Z=!1;s(X,async(e,t)=>{if(!Z&&e>0){Z=!0,await Y();return}e>t&&I.value&&!L.value&&ue()}),i(()=>{K!=null&&(cancelAnimationFrame(K),K=null)}),s(()=>v.workspaceId,()=>{I.value=!0,Z=!1,R=!1,ge(),X.value>0&&Y()}),s(()=>x.selectedSessionId,async()=>{I.value=!0,R=!1,await Y(),$()});let Q=new Set;async function $(){let t=C.value;if(!t||X.value>0)return;let n=V(v.workspaceId,t);if(!Q.has(n)){Q.add(n);try{let n=await fetch(`/api/workspaces/${v.workspaceId}/events?session=${encodeURIComponent(t)}&limit=500`);if(!n.ok)return;let r=await n.json(),i=r.events??[];if(i.length===0)return;let a=i.filter(e=>e.type===`agent:event`&&e.workspaceId===v.workspaceId),o=i.filter(e=>e.type===`user:message`&&e.workspaceId===v.workspaceId),s=a.map(e=>e.payload),c=a.map(e=>e.createdAt),l=a.map(e=>e.sessionId??null),u=a.map(e=>e.id);U(v.workspaceId,t,r.hasMore),s.length>0&&y.prepend(v.workspaceId,s,c,{oldestId:i[0].id,hasMoreOlder:y.hasMoreOlderFor(v.workspaceId),sessionIds:l,eventIds:u});for(let e of o){let t=e.payload;typeof t.content==`string`&&x.addActivityItem(v.workspaceId,{id:e.id,type:`text`,content:t.content,timestamp:e.createdAt,sessionId:e.sessionId??void 0,meta:{sender:t.sender??`user`}})}await e(),await G(0)}catch(e){console.error(`[ActivityFeed] fetchSessionIfMissing failed:`,e),Q.delete(n)}}}s(m(()=>N.value.filter(e=>e.sender!==`system-prompt`).length),async(e,t)=>{e>t&&(I.value=!0,await G(180))});async function _e(){I.value=!0,await G(250)}return(e,r)=>z.value?(n(),h(`div`,Ae,[c(M,{size:`40px`,color:`indigo-4`})])):(n(),h(`div`,je,[c(te,{ref_key:`scrollRef`,ref:F,class:`activity-feed-scroll`,onScroll:se},{default:t(()=>[_(`div`,{ref_key:`contentOriginRef`,ref:q,class:`content-origin-marker`},null,512),L.value?(n(),h(`div`,Me,[c(S,{size:`sm`}),p(` `+o(e.$t(`activity.loading_older`)),1)])):f(``,!0),_(`div`,Ne,[(n(!0),h(g,null,a(re.value,(e,t)=>(n(),u(ke,{key:t,ref_for:!0,ref_key:`turnRefs`,ref:fe,turn:e,onScrollTo:de},null,8,[`turn`]))),128))]),P.value.length?(n(),h(`div`,Pe,[c(ee,{label:e.$t(`activity.raw_lines`,{n:P.value.length}),dense:``},{default:t(()=>[(n(!0),h(g,null,a(P.value,(e,t)=>(n(),h(`div`,{key:t,class:`text-caption text-grey q-pa-xs`},o(e),1))),128))]),_:1},8,[`label`])])):f(``,!0)]),_:1},512),_(`div`,Fe,[I.value?f(``,!0):(n(),u(E,{key:0,round:``,dense:``,unelevated:``,color:`grey-9`,"text-color":`grey-3`,icon:`arrow_downward`,size:`sm`,class:`activity-feed-nav-btn`,title:e.$t(`activity.scroll_to_bottom`),onClick:_e},null,8,[`title`])),c(E,{round:``,dense:``,unelevated:``,color:`grey-9`,"text-color":`grey-3`,icon:`arrow_upward`,size:`sm`,class:`activity-feed-nav-btn`,title:e.$t(`activity.prev_user_message`),onClick:me},null,8,[`title`])])]))}}),[[`__scopeId`,`data-v-c133a623`]]);export{He as default};
@@ -1 +1 @@
1
- import{F as e,b as t}from"./QIcon-BmEX2rXO.js";import{T as n,w as r}from"./notifications-BC6en6Lt.js";function i(e){if(e===!1)return 0;if(e===!0||e===void 0)return 1;let t=parseInt(e,10);return isNaN(t)?0:t}var a=e({name:`close-popup`,beforeMount(e,{value:a}){let o={depth:i(a),handler(t){o.depth!==0&&setTimeout(()=>{let i=n(e);i!==void 0&&r(i,t,o.depth)})},handlerKey(e){t(e,13)===!0&&o.handler(e)}};e.__qclosepopup=o,e.addEventListener(`click`,o.handler),e.addEventListener(`keyup`,o.handlerKey)},updated(e,{value:t,oldValue:n}){t!==n&&(e.__qclosepopup.depth=i(t))},beforeUnmount(e){let t=e.__qclosepopup;e.removeEventListener(`click`,t.handler),e.removeEventListener(`keyup`,t.handlerKey),delete e.__qclosepopup}});export{a as t};
1
+ import{F as e,b as t}from"./QIcon-BmEX2rXO.js";import{T as n,w as r}from"./notifications-Hq-6rEYv.js";function i(e){if(e===!1)return 0;if(e===!0||e===void 0)return 1;let t=parseInt(e,10);return isNaN(t)?0:t}var a=e({name:`close-popup`,beforeMount(e,{value:a}){let o={depth:i(a),handler(t){o.depth!==0&&setTimeout(()=>{let i=n(e);i!==void 0&&r(i,t,o.depth)})},handlerKey(e){t(e,13)===!0&&o.handler(e)}};e.__qclosepopup=o,e.addEventListener(`click`,o.handler),e.addEventListener(`keyup`,o.handlerKey)},updated(e,{value:t,oldValue:n}){t!==n&&(e.__qclosepopup.depth=i(t))},beforeUnmount(e){let t=e.__qclosepopup;e.removeEventListener(`click`,t.handler),e.removeEventListener(`keyup`,t.handlerKey),delete e.__qclosepopup}});export{a as t};