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.
- package/README.md +80 -103
- package/README.zh-CN.md +80 -103
- package/dist/types/common.d.ts +25 -0
- package/dist/types/common.d.ts.map +1 -1
- package/dist/types/common.js +1 -0
- package/dist/types/common.js.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.adc992cd.js.gz +0 -0
- package/dist/web/public/constants.e7175b95.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/i18n.4dcf1b87.js.gz +0 -0
- package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
- package/dist/web/public/index.html +10 -7
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.3f91acef.js → keyboard-accessory.9936227d.js} +361 -2
- package/dist/web/public/keyboard-accessory.9936227d.js.br +0 -0
- package/dist/web/public/keyboard-accessory.9936227d.js.gz +0 -0
- package/dist/web/public/mobile-handlers.bc2b8c1d.js.gz +0 -0
- package/dist/web/public/mobile.40e2d64e.css +1 -0
- package/dist/web/public/mobile.40e2d64e.css.br +0 -0
- package/dist/web/public/mobile.40e2d64e.css.gz +0 -0
- package/dist/web/public/notification-manager.fca811fd.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/panels-ui.3910e1e2.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/{session-ui.fd39c53c.js → session-ui.ab6dd9aa.js} +15 -15
- package/dist/web/public/session-ui.ab6dd9aa.js.br +0 -0
- package/dist/web/public/session-ui.ab6dd9aa.js.gz +0 -0
- package/dist/web/public/settings-ui.b8def032.js.gz +0 -0
- package/dist/web/public/{styles.bd210c71.css → styles.42dd71e1.css} +1 -1
- package/dist/web/public/styles.42dd71e1.css.br +0 -0
- package/dist/web/public/styles.42dd71e1.css.gz +0 -0
- package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/{terminal-ui.feaf75c2.js → terminal-ui.f3a51974.js} +1 -1
- package/dist/web/public/terminal-ui.f3a51974.js.br +0 -0
- package/dist/web/public/terminal-ui.f3a51974.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
- package/dist/web/public/webview-tabs.js +43 -13
- package/dist/web/public/webview-tabs.js.br +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/routes/file-routes.d.ts.map +1 -1
- package/dist/web/routes/file-routes.js +313 -8
- package/dist/web/routes/file-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +173 -99
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +10 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +30 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/webview-proxy.d.ts +18 -1
- package/dist/web/webview-proxy.d.ts.map +1 -1
- package/dist/web/webview-proxy.js +129 -1
- package/dist/web/webview-proxy.js.map +1 -1
- package/package.json +17 -6
- package/dist/web/public/keyboard-accessory.3f91acef.js.br +0 -0
- package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
- package/dist/web/public/mobile.6153d275.css +0 -1
- package/dist/web/public/mobile.6153d275.css.br +0 -0
- package/dist/web/public/mobile.6153d275.css.gz +0 -0
- package/dist/web/public/session-ui.fd39c53c.js.br +0 -0
- package/dist/web/public/session-ui.fd39c53c.js.gz +0 -0
- package/dist/web/public/styles.bd210c71.css.br +0 -0
- package/dist/web/public/styles.bd210c71.css.gz +0 -0
- package/dist/web/public/terminal-ui.feaf75c2.js.br +0 -0
- 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
|
-
|
|
1014
|
+
const query = req.query;
|
|
856
1015
|
const claudeSessionId = session.claudeSessionId || session.id;
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
897
|
-
|
|
1026
|
+
try {
|
|
1027
|
+
const content = await fs.readFile(transcript.path, 'utf8');
|
|
1028
|
+
return parseClaudeResponseTranscript(content, query.context === 'full');
|
|
898
1029
|
}
|
|
899
|
-
|
|
900
|
-
|
|
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) ||
|