aicodeman 1.8.3 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +80 -103
  2. package/README.zh-CN.md +80 -103
  3. package/dist/types/common.d.ts +25 -0
  4. package/dist/types/common.d.ts.map +1 -1
  5. package/dist/types/common.js +1 -0
  6. package/dist/types/common.js.map +1 -1
  7. package/dist/web/public/admin-ui.js.gz +0 -0
  8. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  9. package/dist/web/public/app.adc992cd.js.gz +0 -0
  10. package/dist/web/public/constants.e7175b95.js.gz +0 -0
  11. package/dist/web/public/cron-ui.js.gz +0 -0
  12. package/dist/web/public/i18n.4dcf1b87.js.gz +0 -0
  13. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  14. package/dist/web/public/index.html +10 -7
  15. package/dist/web/public/index.html.br +0 -0
  16. package/dist/web/public/index.html.gz +0 -0
  17. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  18. package/dist/web/public/{keyboard-accessory.3f91acef.js → keyboard-accessory.9936227d.js} +361 -2
  19. package/dist/web/public/keyboard-accessory.9936227d.js.br +0 -0
  20. package/dist/web/public/keyboard-accessory.9936227d.js.gz +0 -0
  21. package/dist/web/public/mobile-handlers.bc2b8c1d.js.gz +0 -0
  22. package/dist/web/public/mobile.40e2d64e.css +1 -0
  23. package/dist/web/public/mobile.40e2d64e.css.br +0 -0
  24. package/dist/web/public/mobile.40e2d64e.css.gz +0 -0
  25. package/dist/web/public/notification-manager.fca811fd.js.gz +0 -0
  26. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  27. package/dist/web/public/panels-ui.3910e1e2.js.gz +0 -0
  28. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  29. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  30. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  31. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  32. package/dist/web/public/{session-ui.fd39c53c.js → session-ui.ab6dd9aa.js} +15 -15
  33. package/dist/web/public/session-ui.ab6dd9aa.js.br +0 -0
  34. package/dist/web/public/session-ui.ab6dd9aa.js.gz +0 -0
  35. package/dist/web/public/settings-ui.b8def032.js.gz +0 -0
  36. package/dist/web/public/{styles.bd210c71.css → styles.42dd71e1.css} +1 -1
  37. package/dist/web/public/styles.42dd71e1.css.br +0 -0
  38. package/dist/web/public/styles.42dd71e1.css.gz +0 -0
  39. package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
  40. package/dist/web/public/sw.js.gz +0 -0
  41. package/dist/web/public/{terminal-ui.feaf75c2.js → terminal-ui.f3a51974.js} +1 -1
  42. package/dist/web/public/terminal-ui.f3a51974.js.br +0 -0
  43. package/dist/web/public/terminal-ui.f3a51974.js.gz +0 -0
  44. package/dist/web/public/ultracode-panel.js.gz +0 -0
  45. package/dist/web/public/ultracode-windows.js.gz +0 -0
  46. package/dist/web/public/upload.html.gz +0 -0
  47. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  48. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  49. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  50. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  51. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  52. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  53. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  54. package/dist/web/public/vendor/xterm.css.gz +0 -0
  55. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  56. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  57. package/dist/web/public/webview-tabs.js +43 -13
  58. package/dist/web/public/webview-tabs.js.br +0 -0
  59. package/dist/web/public/webview-tabs.js.gz +0 -0
  60. package/dist/web/routes/file-routes.d.ts.map +1 -1
  61. package/dist/web/routes/file-routes.js +313 -8
  62. package/dist/web/routes/file-routes.js.map +1 -1
  63. package/dist/web/routes/session-routes.d.ts.map +1 -1
  64. package/dist/web/routes/session-routes.js +173 -99
  65. package/dist/web/routes/session-routes.js.map +1 -1
  66. package/dist/web/schemas.d.ts +10 -0
  67. package/dist/web/schemas.d.ts.map +1 -1
  68. package/dist/web/schemas.js +30 -0
  69. package/dist/web/schemas.js.map +1 -1
  70. package/dist/web/webview-proxy.d.ts +18 -1
  71. package/dist/web/webview-proxy.d.ts.map +1 -1
  72. package/dist/web/webview-proxy.js +129 -1
  73. package/dist/web/webview-proxy.js.map +1 -1
  74. package/package.json +17 -6
  75. package/dist/web/public/keyboard-accessory.3f91acef.js.br +0 -0
  76. package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
  77. package/dist/web/public/mobile.6153d275.css +0 -1
  78. package/dist/web/public/mobile.6153d275.css.br +0 -0
  79. package/dist/web/public/mobile.6153d275.css.gz +0 -0
  80. package/dist/web/public/session-ui.fd39c53c.js.br +0 -0
  81. package/dist/web/public/session-ui.fd39c53c.js.gz +0 -0
  82. package/dist/web/public/styles.bd210c71.css.br +0 -0
  83. package/dist/web/public/styles.bd210c71.css.gz +0 -0
  84. package/dist/web/public/terminal-ui.feaf75c2.js.br +0 -0
  85. package/dist/web/public/terminal-ui.feaf75c2.js.gz +0 -0
@@ -829,6 +829,165 @@ export function registerSessionRoutes(app, ctx) {
829
829
  }
830
830
  return candidateSid;
831
831
  }
832
+ function extractClaudeText(content, separator) {
833
+ if (typeof content === 'string')
834
+ return content;
835
+ if (!Array.isArray(content))
836
+ return '';
837
+ return content
838
+ .filter((block) => !!block &&
839
+ typeof block === 'object' &&
840
+ block.type === 'text' &&
841
+ typeof block.text === 'string')
842
+ .map((block) => block.text)
843
+ .join(separator);
844
+ }
845
+ function isClaudeSyntheticUserMessage(entry, text) {
846
+ if (entry.isMeta || entry.isCompactSummary)
847
+ return true;
848
+ return /^(?:<local-command|<command-name>|<task-notification>|<system-reminder>|<teammate-message\b|Another Claude session sent a message:|Base directory for this skill:)/i.test(text);
849
+ }
850
+ /**
851
+ * Claude writes one logical turn as many JSONL rows: text, thinking and tool
852
+ * blocks share message ids, while tool results are represented as user rows.
853
+ * Build viewer cards from real user boundaries instead of treating every row
854
+ * as a separate chat message.
855
+ */
856
+ function parseClaudeResponseTranscript(content, full) {
857
+ let lastText = '';
858
+ let lastTimestamp = '';
859
+ const messages = [];
860
+ let currentUserFragments = new Set();
861
+ let currentAssistantFragments = new Set();
862
+ for (const line of content.split('\n')) {
863
+ if (!line)
864
+ continue;
865
+ let entry;
866
+ try {
867
+ entry = JSON.parse(line);
868
+ }
869
+ catch {
870
+ continue;
871
+ }
872
+ // Sidechains belong to agents/forks, not the main conversation. Meta user
873
+ // rows include repeated image dimensions and other UI-generated context.
874
+ if (entry.isSidechain)
875
+ continue;
876
+ if (entry.type === 'user') {
877
+ const text = extractClaudeText(entry.message?.content, '\n').trim();
878
+ // A tool_result block has no text block and naturally drops out here.
879
+ if (!text || isClaudeSyntheticUserMessage(entry, text))
880
+ continue;
881
+ if (!full)
882
+ continue;
883
+ const previous = messages.at(-1);
884
+ if (previous?.role === 'user') {
885
+ // Claude can replay the initial user row while restoring a transcript.
886
+ // Only collapse duplicates within the same unanswered user turn; the
887
+ // same prompt after an assistant response remains a legitimate turn.
888
+ if (currentUserFragments.has(text))
889
+ continue;
890
+ previous.text += `\n\n${text}`;
891
+ currentUserFragments.add(text);
892
+ }
893
+ else {
894
+ messages.push({ role: 'user', text, timestamp: entry.timestamp });
895
+ currentUserFragments = new Set([text]);
896
+ }
897
+ currentAssistantFragments.clear();
898
+ continue;
899
+ }
900
+ if (entry.type !== 'assistant')
901
+ continue;
902
+ const text = extractClaudeText(entry.message?.content, '\n\n').trim();
903
+ if (!text)
904
+ continue;
905
+ lastText = text;
906
+ lastTimestamp = entry.timestamp || '';
907
+ if (!full)
908
+ continue;
909
+ const previous = messages.at(-1);
910
+ if (previous?.role === 'assistant') {
911
+ // Replayed snapshots sometimes repeat an identical text block. Distinct
912
+ // progress/final blocks are kept, but remain inside one Claude card.
913
+ if (currentAssistantFragments.has(text))
914
+ continue;
915
+ previous.text += `\n\n${text}`;
916
+ previous.timestamp = entry.timestamp || previous.timestamp;
917
+ currentAssistantFragments.add(text);
918
+ }
919
+ else {
920
+ messages.push({ role: 'assistant', text, timestamp: entry.timestamp });
921
+ currentAssistantFragments = new Set([text]);
922
+ }
923
+ currentUserFragments.clear();
924
+ }
925
+ return full ? { text: lastText, timestamp: lastTimestamp, messages } : { text: lastText, timestamp: lastTimestamp };
926
+ }
927
+ /** Locate a top-level Claude transcript, including recovered tmux sessions. */
928
+ async function findClaudeTranscript(projectsDir, conversationId, codemanSessionId) {
929
+ let projectDirs;
930
+ try {
931
+ projectDirs = await fs.readdir(projectsDir, { withFileTypes: true });
932
+ }
933
+ catch {
934
+ return null;
935
+ }
936
+ const safeIds = [...new Set([conversationId, codemanSessionId])].filter((value) => /^[a-zA-Z0-9._-]+$/.test(value));
937
+ for (const candidateId of safeIds) {
938
+ for (const projectDir of projectDirs) {
939
+ if (!projectDir.isDirectory())
940
+ continue;
941
+ const jsonlPath = join(projectsDir, projectDir.name, `${candidateId}.jsonl`);
942
+ try {
943
+ const stat = await fs.stat(jsonlPath);
944
+ if (stat.isFile())
945
+ return { sessionId: candidateId, path: jsonlPath };
946
+ }
947
+ catch {
948
+ /* continue */
949
+ }
950
+ }
951
+ }
952
+ // If mux-sessions.json was lost or stale, reconcileSessions() historically
953
+ // recovered `codeman-40568a29` as `restored-40568a29` and used the server cwd.
954
+ // The tmux name still carries the first eight UUID characters, which safely
955
+ // reconnects the viewer when exactly one matching top-level transcript exists.
956
+ const restoredMatch = /^restored-([a-f0-9]{8,})$/i.exec(codemanSessionId);
957
+ if (!restoredMatch)
958
+ return null;
959
+ const fragment = restoredMatch[1].toLowerCase();
960
+ const candidates = [];
961
+ const uuidPattern = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
962
+ for (const projectDir of projectDirs) {
963
+ if (!projectDir.isDirectory())
964
+ continue;
965
+ const dirPath = join(projectsDir, projectDir.name);
966
+ let files;
967
+ try {
968
+ files = await fs.readdir(dirPath, { withFileTypes: true });
969
+ }
970
+ catch {
971
+ continue;
972
+ }
973
+ for (const file of files) {
974
+ if (!file.isFile() || !file.name.endsWith('.jsonl'))
975
+ continue;
976
+ const candidateId = file.name.slice(0, -'.jsonl'.length);
977
+ if (!candidateId.toLowerCase().startsWith(fragment) || !uuidPattern.test(candidateId))
978
+ continue;
979
+ const path = join(dirPath, file.name);
980
+ const stat = await fs.stat(path).catch(() => null);
981
+ if (stat)
982
+ candidates.push({ sessionId: candidateId, path, mtimeMs: stat.mtimeMs });
983
+ }
984
+ }
985
+ const candidateIds = new Set(candidates.map((candidate) => candidate.sessionId));
986
+ if (candidateIds.size !== 1)
987
+ return null;
988
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
989
+ return candidates[0] ?? null;
990
+ }
832
991
  app.get('/api/sessions/:id/last-response', async (req) => {
833
992
  const { id } = req.params;
834
993
  const session = findSessionOrFail(ctx, id, req);
@@ -852,110 +1011,25 @@ export function registerSessionRoutes(app, ctx) {
852
1011
  void persistDockerCaseClaudeSessionId(CODEMAN_CONFIG_DIR, session.docker.containerName, activeId).catch(() => { });
853
1012
  }
854
1013
  }
855
- // The Claude conversation ID (used as JSONL filename)
1014
+ const query = req.query;
856
1015
  const claudeSessionId = session.claudeSessionId || session.id;
857
- let transcriptText = '';
858
- let transcriptTimestamp = '';
859
- try {
860
- const projectDirs = await fs.readdir(projectsDir);
861
- for (const projDir of projectDirs) {
862
- const jsonlPath = join(projectsDir, projDir, `${claudeSessionId}.jsonl`);
863
- try {
864
- const content = await fs.readFile(jsonlPath, 'utf8');
865
- const lines = content.trim().split('\n');
866
- // Search from end for last assistant message with text
867
- for (let i = lines.length - 1; i >= 0; i--) {
868
- try {
869
- const entry = JSON.parse(lines[i]);
870
- if (entry.type === 'assistant' && entry.message?.content) {
871
- const blocks = Array.isArray(entry.message.content)
872
- ? entry.message.content
873
- : [{ type: 'text', text: String(entry.message.content) }];
874
- const textBlocks = blocks
875
- .filter((b) => b.type === 'text' && b.text)
876
- .map((b) => b.text);
877
- if (textBlocks.length > 0) {
878
- transcriptText = textBlocks.join('\n\n');
879
- transcriptTimestamp = entry.timestamp || '';
880
- break;
881
- }
882
- }
883
- }
884
- catch {
885
- // Skip unparseable lines
886
- }
887
- }
888
- if (transcriptText)
889
- break; // Found it, stop scanning directories
890
- }
891
- catch {
892
- // File doesn't exist in this project dir, continue
893
- }
1016
+ const transcript = await findClaudeTranscript(projectsDir, claudeSessionId, session.id);
1017
+ if (!transcript) {
1018
+ return query.context === 'full' ? { text: '', timestamp: '', messages: [] } : { text: '', timestamp: '' };
1019
+ }
1020
+ if (transcript.sessionId !== session.claudeSessionId && transcript.sessionId !== session.id) {
1021
+ session.adoptClaudeSessionId(transcript.sessionId);
1022
+ if (session.docker) {
1023
+ void persistDockerCaseClaudeSessionId(CODEMAN_CONFIG_DIR, session.docker.containerName, transcript.sessionId).catch(() => { });
894
1024
  }
895
1025
  }
896
- catch {
897
- // projects dir doesn't exist
1026
+ try {
1027
+ const content = await fs.readFile(transcript.path, 'utf8');
1028
+ return parseClaudeResponseTranscript(content, query.context === 'full');
898
1029
  }
899
- // If ?context=full, return all user+assistant messages for conversation view
900
- const query = req.query;
901
- if (query.context === 'full' && transcriptText) {
902
- const allMessages = [];
903
- try {
904
- const projectDirs = await fs.readdir(projectsDir);
905
- for (const projDir of projectDirs) {
906
- const jsonlPath = join(projectsDir, projDir, `${claudeSessionId}.jsonl`);
907
- try {
908
- const content = await fs.readFile(jsonlPath, 'utf8');
909
- const lines = content.trim().split('\n');
910
- for (const line of lines) {
911
- try {
912
- const entry = JSON.parse(line);
913
- if (entry.type === 'user' && entry.message?.content) {
914
- const text = typeof entry.message.content === 'string'
915
- ? entry.message.content
916
- : entry.message.content
917
- .filter((b) => b.type === 'text' && b.text)
918
- .map((b) => b.text)
919
- .join('\n');
920
- // Skip system/command messages
921
- if (text && !text.startsWith('<local-command') && !text.startsWith('<command-name>')) {
922
- allMessages.push({ role: 'user', text, timestamp: entry.timestamp });
923
- }
924
- }
925
- else if (entry.type === 'assistant' && entry.message?.content) {
926
- const blocks = Array.isArray(entry.message.content)
927
- ? entry.message.content
928
- : [{ type: 'text', text: String(entry.message.content) }];
929
- const text = blocks
930
- .filter((b) => b.type === 'text' && b.text)
931
- .map((b) => b.text)
932
- .join('\n\n');
933
- if (text) {
934
- allMessages.push({ role: 'assistant', text, timestamp: entry.timestamp });
935
- }
936
- }
937
- }
938
- catch {
939
- /* skip */
940
- }
941
- }
942
- if (allMessages.length > 0)
943
- break;
944
- }
945
- catch {
946
- /* continue */
947
- }
948
- }
949
- }
950
- catch {
951
- /* ignore */
952
- }
953
- return { text: transcriptText, timestamp: transcriptTimestamp, messages: allMessages };
1030
+ catch {
1031
+ return query.context === 'full' ? { text: '', timestamp: '', messages: [] } : { text: '', timestamp: '' };
954
1032
  }
955
- return {
956
- text: transcriptText,
957
- timestamp: transcriptTimestamp,
958
- };
959
1033
  });
960
1034
  function isCodexInjectedContext(text) {
961
1035
  return (/^# AGENTS\.md instructions\b/i.test(text) ||