@openchamber/web 1.11.6 → 1.11.7

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 (47) hide show
  1. package/README.md +6 -0
  2. package/bin/cli.js +443 -2
  3. package/dist/assets/{MarkdownRendererImpl-COdbjw73.js → MarkdownRendererImpl-DaF15QNC.js} +3 -3
  4. package/dist/assets/{MultiRunWindow-BKSHxjMq.js → MultiRunWindow-Cl7wS_CB.js} +1 -1
  5. package/dist/assets/{OnboardingScreen-Chjg337p.js → OnboardingScreen-DTv6YJI1.js} +2 -2
  6. package/dist/assets/{SettingsWindow-C0lRRW8M.js → SettingsWindow-_c3TTL2z.js} +1 -1
  7. package/dist/assets/{TerminalView-Bvil3j1u.js → TerminalView-CuXkDROt.js} +3 -3
  8. package/dist/assets/es-CYoUf2D-.js +15 -0
  9. package/dist/assets/{index-B9LvUHdG.js → index-3WXrN3AX.js} +1 -1
  10. package/dist/assets/index-BREIbhcb.css +1 -0
  11. package/dist/assets/ko-2tM0fIna.js +15 -0
  12. package/dist/assets/main-BF3kWAJ9.js +239 -0
  13. package/dist/assets/{main-Blhx9Fp5.js → main-o8ZERrmU.js} +2 -2
  14. package/dist/assets/miniChat-BZQjpK23.js +2 -0
  15. package/dist/assets/{modelPrefsAutoSave-DRJSYigo.js → modelPrefsAutoSave-wwnbqBk7.js} +110 -108
  16. package/dist/assets/pl-Dq8uAotM.js +15 -0
  17. package/dist/assets/pt-BR-nh9s9DFT.js +15 -0
  18. package/dist/assets/{renderElectronMiniChatApp-BxZRI73j.js → renderElectronMiniChatApp-C-Ezew9P.js} +2 -2
  19. package/dist/assets/uk-BZtz0wUV.js +15 -0
  20. package/dist/assets/{vendor-.bun-Bum-iBXX.js → vendor-.bun-CV3tusA8.js} +1 -1
  21. package/dist/assets/zh-CN-j_nYMchE.js +15 -0
  22. package/dist/assets/zh-TW-B11UpkDJ.js +15 -0
  23. package/dist/index.html +11 -28
  24. package/dist/mini-chat.html +4 -4
  25. package/package.json +1 -1
  26. package/server/lib/fs/routes.js +5 -0
  27. package/server/lib/fs/routes.test.js +61 -1
  28. package/server/lib/git/DOCUMENTATION.md +1 -0
  29. package/server/lib/git/routes.js +82 -1
  30. package/server/lib/git/service.js +338 -19
  31. package/server/lib/git/service.test.js +414 -8
  32. package/server/lib/opencode/env-runtime.js +52 -4
  33. package/server/lib/opencode/env-runtime.test.js +82 -6
  34. package/server/lib/opencode/openchamber-routes.js +9 -7
  35. package/server/lib/opencode/settings-helpers.js +3 -0
  36. package/server/lib/opencode/settings-runtime.js +39 -1
  37. package/server/lib/opencode/settings-runtime.test.js +39 -0
  38. package/server/lib/skills-catalog/source.js +1 -1
  39. package/dist/assets/es-BZIAUghG.js +0 -15
  40. package/dist/assets/index-UcCH2KN9.css +0 -1
  41. package/dist/assets/ko-DU9l-zox.js +0 -15
  42. package/dist/assets/main-d2-dY4er.js +0 -232
  43. package/dist/assets/miniChat-CJ7-rZFl.js +0 -2
  44. package/dist/assets/pl-CdqzokG-.js +0 -15
  45. package/dist/assets/pt-BR-Bknbr_Y3.js +0 -15
  46. package/dist/assets/uk-Be4E8ZNO.js +0 -15
  47. package/dist/assets/zh-CN-qpPiaZMg.js +0 -15
package/dist/index.html CHANGED
@@ -2,7 +2,7 @@
2
2
  <html lang="en" class="h-full">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" />
6
6
 
7
7
  <!-- Favicon -->
8
8
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
@@ -57,46 +57,29 @@
57
57
  return fallback;
58
58
  };
59
59
 
60
- const normalizeMobileKeyboardMode = (value, fallback = 'native') => {
60
+ const normalizeMobileKeyboardMode = (value, fallback = 'resize-content') => {
61
61
  if (value === 'native' || value === 'resize-content') {
62
62
  return value;
63
63
  }
64
64
  return fallback;
65
65
  };
66
66
 
67
- const supportsMobileKeyboardResizeContent = () => {
68
- const userAgent = navigator.userAgent || '';
69
- const platform = navigator.platform || '';
70
- const maxTouchPoints = navigator.maxTouchPoints || 0;
71
- const isIOS = /iPhone|iPad|iPod/i.test(userAgent)
72
- || ((/Macintosh|MacIntel/i.test(userAgent) || /MacIntel/i.test(platform)) && maxTouchPoints > 1);
73
-
74
- return !isIOS;
75
- };
76
-
77
- const getSupportedMobileKeyboardMode = (mode) => {
78
- if (mode === 'resize-content' && !supportsMobileKeyboardResizeContent()) {
79
- return 'native';
80
- }
81
- return mode;
82
- };
83
-
84
67
  const getViewportContentForMobileKeyboardMode = (value) => {
85
- const mode = getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(value, 'native'));
68
+ const mode = normalizeMobileKeyboardMode(value, 'resize-content');
86
69
  return mode === 'resize-content'
87
70
  ? `${viewportBaseContent}, interactive-widget=resizes-content`
88
71
  : viewportBaseContent;
89
72
  };
90
73
 
91
74
  const applyStoredMobileKeyboardMode = () => {
92
- let mode = 'native';
75
+ let mode = 'resize-content';
93
76
  try {
94
- mode = getSupportedMobileKeyboardMode(normalizeMobileKeyboardMode(localStorage.getItem(mobileKeyboardModeStorageKey), 'native'));
95
- if (mode === 'native') {
77
+ mode = normalizeMobileKeyboardMode(localStorage.getItem(mobileKeyboardModeStorageKey), 'resize-content');
78
+ if (mode === 'resize-content') {
96
79
  localStorage.removeItem(mobileKeyboardModeStorageKey);
97
80
  }
98
81
  } catch {
99
- mode = 'native';
82
+ mode = 'resize-content';
100
83
  }
101
84
 
102
85
  document.documentElement.setAttribute('data-oc-mobile-keyboard-mode', mode);
@@ -532,10 +515,10 @@
532
515
  pointer-events: none;
533
516
  }
534
517
  </style>
535
- <script type="module" crossorigin src="/assets/main-Blhx9Fp5.js"></script>
536
- <link rel="modulepreload" crossorigin href="/assets/index-B9LvUHdG.js">
537
- <link rel="modulepreload" crossorigin href="/assets/vendor-.bun-Bum-iBXX.js">
538
- <link rel="stylesheet" crossorigin href="/assets/index-UcCH2KN9.css">
518
+ <script type="module" crossorigin src="/assets/main-o8ZERrmU.js"></script>
519
+ <link rel="modulepreload" crossorigin href="/assets/index-3WXrN3AX.js">
520
+ <link rel="modulepreload" crossorigin href="/assets/vendor-.bun-CV3tusA8.js">
521
+ <link rel="stylesheet" crossorigin href="/assets/index-BREIbhcb.css">
539
522
  <link rel="stylesheet" crossorigin href="/assets/vendor--V65Sl9C2.css">
540
523
  </head>
541
524
  <body class="h-full bg-background text-foreground">
@@ -4,10 +4,10 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
6
6
  <title>OpenChamber Mini Chat</title>
7
- <script type="module" crossorigin src="/assets/miniChat-CJ7-rZFl.js"></script>
8
- <link rel="modulepreload" crossorigin href="/assets/index-B9LvUHdG.js">
9
- <link rel="modulepreload" crossorigin href="/assets/vendor-.bun-Bum-iBXX.js">
10
- <link rel="stylesheet" crossorigin href="/assets/index-UcCH2KN9.css">
7
+ <script type="module" crossorigin src="/assets/miniChat-BZQjpK23.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/assets/index-3WXrN3AX.js">
9
+ <link rel="modulepreload" crossorigin href="/assets/vendor-.bun-CV3tusA8.js">
10
+ <link rel="stylesheet" crossorigin href="/assets/index-BREIbhcb.css">
11
11
  <link rel="stylesheet" crossorigin href="/assets/vendor--V65Sl9C2.css">
12
12
  </head>
13
13
  <body class="h-full bg-background text-foreground">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchamber/web",
3
- "version": "1.11.6",
3
+ "version": "1.11.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -772,6 +772,11 @@ export const registerFsRoutes = (app, dependencies) => {
772
772
  return res.status(400).json({ error: resolved.error });
773
773
  }
774
774
 
775
+ const existing = await fsPromises.readFile(resolved.resolved, 'utf8').catch(() => null);
776
+ if (existing === content) {
777
+ return res.json({ success: true, path: resolved.resolved });
778
+ }
779
+
775
780
  await fsPromises.mkdir(path.dirname(resolved.resolved), { recursive: true });
776
781
  await fsPromises.writeFile(resolved.resolved, content, 'utf8');
777
782
  return res.json({ success: true, path: resolved.resolved });
@@ -90,7 +90,10 @@ const registerExec = ({ spawn }) => {
90
90
  registerFsRoutes(app, {
91
91
  os: { homedir: () => '/home/user' },
92
92
  path,
93
- fsPromises: { stat: async () => ({ isDirectory: () => true }) },
93
+ fsPromises: {
94
+ realpath: async (targetPath) => targetPath,
95
+ stat: async () => ({ isDirectory: () => true }),
96
+ },
94
97
  spawn,
95
98
  crypto: { randomUUID: (() => { let n = 0; return () => `job-${n++}`; })() },
96
99
  normalizeDirectoryPath: (p) => p,
@@ -102,12 +105,69 @@ const registerExec = ({ spawn }) => {
102
105
  return getRoute('POST', '/api/fs/exec');
103
106
  };
104
107
 
108
+ const registerWrite = (fsPromises) => {
109
+ const { app, getRoute } = createRouteRegistry();
110
+ registerFsRoutes(app, {
111
+ os: { homedir: () => '/home/user' },
112
+ path: path.posix,
113
+ fsPromises: {
114
+ realpath: async (targetPath) => targetPath,
115
+ ...fsPromises,
116
+ },
117
+ spawn: vi.fn(),
118
+ crypto: { randomUUID: () => 'job-0' },
119
+ normalizeDirectoryPath: (p) => p,
120
+ resolveProjectDirectory: async () => ({ directory: '/repo' }),
121
+ buildAugmentedPath: () => '/usr/bin',
122
+ resolveGitBinaryForSpawn: () => 'git',
123
+ openchamberUserConfigRoot: '/home/user/.config',
124
+ });
125
+ return getRoute('POST', '/api/fs/write');
126
+ };
127
+
105
128
  const callExec = async (handler, body) => {
106
129
  const res = createMockResponse();
107
130
  await handler({ body }, res);
108
131
  return res;
109
132
  };
110
133
 
134
+ const callWrite = async (handler, body) => {
135
+ const res = createMockResponse();
136
+ await handler({ body }, res);
137
+ return res;
138
+ };
139
+
140
+ describe('fs write', () => {
141
+ it('does not rewrite a file when content is unchanged', async () => {
142
+ const fsPromises = {
143
+ readFile: vi.fn(async () => 'same'),
144
+ mkdir: vi.fn(async () => undefined),
145
+ writeFile: vi.fn(async () => undefined),
146
+ };
147
+ const handler = registerWrite(fsPromises);
148
+
149
+ const res = await callWrite(handler, { path: '/repo/file.txt', content: 'same' });
150
+
151
+ expect(res.body).toEqual({ success: true, path: '/repo/file.txt' });
152
+ expect(fsPromises.writeFile).not.toHaveBeenCalled();
153
+ });
154
+
155
+ it('writes a file when content changed', async () => {
156
+ const fsPromises = {
157
+ readFile: vi.fn(async () => 'old'),
158
+ mkdir: vi.fn(async () => undefined),
159
+ writeFile: vi.fn(async () => undefined),
160
+ };
161
+ const handler = registerWrite(fsPromises);
162
+
163
+ const res = await callWrite(handler, { path: '/repo/file.txt', content: 'new' });
164
+
165
+ expect(res.body).toEqual({ success: true, path: '/repo/file.txt' });
166
+ expect(fsPromises.mkdir).toHaveBeenCalledWith('/repo', { recursive: true });
167
+ expect(fsPromises.writeFile).toHaveBeenCalledWith('/repo/file.txt', 'new', 'utf8');
168
+ });
169
+ });
170
+
111
171
  describe('fs exec git-read cache', () => {
112
172
  beforeEach(() => {
113
173
  delete process.env.OPENCHAMBER_GIT_READ_CACHE_TTL_MS;
@@ -101,6 +101,7 @@ The following functions are internal helpers used by exported functions:
101
101
  - `tracking`: Upstream branch (e.g., 'origin/main').
102
102
  - `ahead`: Number of commits ahead of upstream.
103
103
  - `behind`: Number of commits behind upstream.
104
+ - `upstreamComparison`: Optional comparison against `upstream/<current-branch>`, with `{ remote, branch, ahead, behind }`.
104
105
  - `files`: Array of file objects with `path`, `index`, `working_dir` status codes.
105
106
  - `isClean`: Boolean indicating if working tree is clean.
106
107
  - `diffStats`: Object mapping file paths to `{ insertions, deletions }`.
@@ -766,6 +766,85 @@ export function registerGitRoutes(app) {
766
766
  }
767
767
  });
768
768
 
769
+ app.post('/api/git/checkout-commit', async (req, res) => {
770
+ const { checkoutCommit } = await getGitLibraries();
771
+ try {
772
+ const directory = req.query.directory;
773
+ if (!directory) {
774
+ return res.status(400).json({ error: 'directory parameter is required' });
775
+ }
776
+ const { hash } = req.body;
777
+ if (!req.body.hash || typeof req.body.hash !== 'string' || !/^[0-9a-fA-F]{7,40}$/.test(req.body.hash)) {
778
+ return res.status(400).json({ error: 'Invalid commit hash' });
779
+ }
780
+ const result = await checkoutCommit(directory, hash);
781
+ res.json(result);
782
+ } catch (error) {
783
+ console.error('Failed to checkout commit:', error);
784
+ res.status(500).json({ error: error.message || 'Failed to checkout commit' });
785
+ }
786
+ });
787
+
788
+ app.post('/api/git/cherry-pick', async (req, res) => {
789
+ const { cherryPick } = await getGitLibraries();
790
+ try {
791
+ const directory = req.query.directory;
792
+ if (!directory) {
793
+ return res.status(400).json({ error: 'directory parameter is required' });
794
+ }
795
+ const { hash } = req.body;
796
+ if (!req.body.hash || typeof req.body.hash !== 'string' || !/^[0-9a-fA-F]{7,40}$/.test(req.body.hash)) {
797
+ return res.status(400).json({ error: 'Invalid commit hash' });
798
+ }
799
+ const result = await cherryPick(directory, hash);
800
+ res.json(result);
801
+ } catch (error) {
802
+ console.error('Failed to cherry-pick:', error);
803
+ res.status(500).json({ error: error.message || 'Failed to cherry-pick' });
804
+ }
805
+ });
806
+
807
+ app.post('/api/git/revert-commit', async (req, res) => {
808
+ const { revertCommit } = await getGitLibraries();
809
+ try {
810
+ const directory = req.query.directory;
811
+ if (!directory) {
812
+ return res.status(400).json({ error: 'directory parameter is required' });
813
+ }
814
+ const { hash } = req.body;
815
+ if (!req.body.hash || typeof req.body.hash !== 'string' || !/^[0-9a-fA-F]{7,40}$/.test(req.body.hash)) {
816
+ return res.status(400).json({ error: 'Invalid commit hash' });
817
+ }
818
+ const result = await revertCommit(directory, hash);
819
+ res.json(result);
820
+ } catch (error) {
821
+ console.error('Failed to revert commit:', error);
822
+ res.status(500).json({ error: error.message || 'Failed to revert commit' });
823
+ }
824
+ });
825
+
826
+ app.post('/api/git/reset-to-commit', async (req, res) => {
827
+ const { resetToCommit } = await getGitLibraries();
828
+ try {
829
+ const directory = req.query.directory;
830
+ if (!directory) {
831
+ return res.status(400).json({ error: 'directory parameter is required' });
832
+ }
833
+ const { hash, mode, force } = req.body;
834
+ if (!req.body.hash || typeof req.body.hash !== 'string' || !/^[0-9a-fA-F]{7,40}$/.test(req.body.hash)) {
835
+ return res.status(400).json({ error: 'Invalid commit hash' });
836
+ }
837
+ if (!['soft', 'mixed', 'hard'].includes(mode)) {
838
+ return res.status(400).json({ error: 'mode must be soft, mixed, or hard' });
839
+ }
840
+ const result = await resetToCommit(directory, hash, mode, force === true);
841
+ res.json(result);
842
+ } catch (error) {
843
+ console.error('Failed to reset to commit:', error);
844
+ res.status(500).json({ error: error.message || 'Failed to reset' });
845
+ }
846
+ });
847
+
769
848
  app.get('/api/git/worktrees', async (req, res) => {
770
849
  const { getWorktrees } = await getGitLibraries();
771
850
  try {
@@ -956,11 +1035,13 @@ export function registerGitRoutes(app) {
956
1035
  }
957
1036
 
958
1037
  const { maxCount, from, to, file } = req.query;
1038
+ const all = req.query.all === 'true';
959
1039
  const log = await getLog(directory, {
960
1040
  maxCount: maxCount ? parseInt(maxCount) : undefined,
961
1041
  from,
962
1042
  to,
963
- file
1043
+ file,
1044
+ all
964
1045
  });
965
1046
  res.json(log);
966
1047
  } catch (error) {