droid-patch 0.9.0 → 0.10.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 +0 -122
- package/README.zh-CN.md +0 -122
- package/dist/{alias-DkFWCjWn.mjs → alias-C2Iew8yJ.mjs} +7 -5
- package/dist/alias-C2Iew8yJ.mjs.map +1 -0
- package/dist/cli.mjs +30 -2718
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/alias-DkFWCjWn.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as removeAlias, d as listAllMetadata, f as loadAliasMetadata, i as listAliases, l as createMetadata, m as patchDroid, n as createAlias, o as removeAliasesByFilter, p as saveAliasMetadata, r as createAliasForWrapper, t as clearAllAliases, u as formatPatches } from "./alias-
|
|
2
|
+
import { a as removeAlias, d as listAllMetadata, f as loadAliasMetadata, i as listAliases, l as createMetadata, m as patchDroid, n as createAlias, o as removeAliasesByFilter, p as saveAliasMetadata, r as createAliasForWrapper, t as clearAllAliases, u as formatPatches } from "./alias-C2Iew8yJ.mjs";
|
|
3
3
|
import bin from "tiny-bin";
|
|
4
4
|
import { styleText } from "node:util";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -662,2675 +662,6 @@ async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName, apiB
|
|
|
662
662
|
};
|
|
663
663
|
}
|
|
664
664
|
|
|
665
|
-
//#endregion
|
|
666
|
-
//#region src/statusline-patch.ts
|
|
667
|
-
function generateStatuslineMonitorScript() {
|
|
668
|
-
return `#!/usr/bin/env node
|
|
669
|
-
/* Auto-generated by droid-patch --statusline */
|
|
670
|
-
|
|
671
|
-
const fs = require('fs');
|
|
672
|
-
const os = require('os');
|
|
673
|
-
const path = require('path');
|
|
674
|
-
const { spawn, spawnSync } = require('child_process');
|
|
675
|
-
|
|
676
|
-
// This monitor does NOT draw directly to the terminal. It emits newline-delimited
|
|
677
|
-
// statusline frames to stdout. A wrapper (PTY proxy) is responsible for rendering
|
|
678
|
-
// the latest frame on a reserved bottom row to avoid flicker.
|
|
679
|
-
|
|
680
|
-
const FACTORY_HOME = path.join(os.homedir(), '.factory');
|
|
681
|
-
|
|
682
|
-
const SESSIONS_ROOT = path.join(FACTORY_HOME, 'sessions');
|
|
683
|
-
const LOG_PATH = path.join(FACTORY_HOME, 'logs', 'droid-log-single.log');
|
|
684
|
-
const CONFIG_PATH = path.join(FACTORY_HOME, 'config.json');
|
|
685
|
-
const GLOBAL_SETTINGS_PATH = path.join(FACTORY_HOME, 'settings.json');
|
|
686
|
-
|
|
687
|
-
const IS_APPLE_TERMINAL = process.env.TERM_PROGRAM === 'Apple_Terminal';
|
|
688
|
-
const MIN_RENDER_INTERVAL_MS = IS_APPLE_TERMINAL ? 1000 : 500;
|
|
689
|
-
|
|
690
|
-
const START_MS = Date.now();
|
|
691
|
-
const ARGS = process.argv.slice(2);
|
|
692
|
-
const PGID = Number(process.env.DROID_STATUSLINE_PGID || '');
|
|
693
|
-
const SESSION_ID_RE = /"sessionId":"([0-9a-f-]{36})"/i;
|
|
694
|
-
|
|
695
|
-
function sleep(ms) {
|
|
696
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function isPositiveInt(n) {
|
|
700
|
-
return Number.isFinite(n) && n > 0;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function extractSessionIdFromLine(line) {
|
|
704
|
-
if (!line) return null;
|
|
705
|
-
const m = String(line).match(SESSION_ID_RE);
|
|
706
|
-
return m ? m[1] : null;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function parseLineTimestampMs(line) {
|
|
710
|
-
const s = String(line || '');
|
|
711
|
-
if (!s || s[0] !== '[') return null;
|
|
712
|
-
const end = s.indexOf(']');
|
|
713
|
-
if (end <= 1) return null;
|
|
714
|
-
const raw = s.slice(1, end);
|
|
715
|
-
const ms = Date.parse(raw);
|
|
716
|
-
return Number.isFinite(ms) ? ms : null;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function safeStatMtimeMs(p) {
|
|
720
|
-
try {
|
|
721
|
-
const stat = fs.statSync(p);
|
|
722
|
-
const ms = Number(stat?.mtimeMs ?? 0);
|
|
723
|
-
return Number.isFinite(ms) ? ms : 0;
|
|
724
|
-
} catch {
|
|
725
|
-
return 0;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function nextCompactionState(line, current) {
|
|
730
|
-
if (!line) return current;
|
|
731
|
-
if (line.includes('[Compaction] Start')) return true;
|
|
732
|
-
const endMarkers = ['End', 'Done', 'Finish', 'Finished', 'Complete', 'Completed'];
|
|
733
|
-
if (endMarkers.some(m => line.includes('[Compaction] ' + m))) return false;
|
|
734
|
-
return current;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function firstNonNull(promises) {
|
|
738
|
-
const list = Array.isArray(promises) ? promises : [];
|
|
739
|
-
if (list.length === 0) return Promise.resolve(null);
|
|
740
|
-
return new Promise((resolve) => {
|
|
741
|
-
let pending = list.length;
|
|
742
|
-
let done = false;
|
|
743
|
-
for (const p of list) {
|
|
744
|
-
Promise.resolve(p)
|
|
745
|
-
.then((value) => {
|
|
746
|
-
if (done) return;
|
|
747
|
-
if (value) {
|
|
748
|
-
done = true;
|
|
749
|
-
resolve(value);
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
pending -= 1;
|
|
753
|
-
if (pending <= 0) resolve(null);
|
|
754
|
-
})
|
|
755
|
-
.catch(() => {
|
|
756
|
-
if (done) return;
|
|
757
|
-
pending -= 1;
|
|
758
|
-
if (pending <= 0) resolve(null);
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
});
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function listPidsInProcessGroup(pgid) {
|
|
765
|
-
if (!isPositiveInt(pgid)) return [];
|
|
766
|
-
try {
|
|
767
|
-
const res = spawnSync('ps', ['-ax', '-o', 'pid=,pgid='], {
|
|
768
|
-
encoding: 'utf8',
|
|
769
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
770
|
-
timeout: 800,
|
|
771
|
-
});
|
|
772
|
-
if (!res || res.status !== 0) return [];
|
|
773
|
-
const out = String(res.stdout || '');
|
|
774
|
-
const pids = [];
|
|
775
|
-
for (const line of out.split('\\n')) {
|
|
776
|
-
const parts = line.trim().split(/\\s+/);
|
|
777
|
-
if (parts.length < 2) continue;
|
|
778
|
-
const pid = Number(parts[0]);
|
|
779
|
-
const g = Number(parts[1]);
|
|
780
|
-
if (Number.isFinite(pid) && g === pgid) pids.push(pid);
|
|
781
|
-
}
|
|
782
|
-
return pids;
|
|
783
|
-
} catch {
|
|
784
|
-
return [];
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
function resolveOpenSessionFromPids(pids) {
|
|
789
|
-
if (!Array.isArray(pids) || pids.length === 0) return null;
|
|
790
|
-
// lsof prints file names as lines prefixed with "n" when using -Fn
|
|
791
|
-
try {
|
|
792
|
-
const res = spawnSync('lsof', ['-p', pids.join(','), '-Fn'], {
|
|
793
|
-
encoding: 'utf8',
|
|
794
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
795
|
-
timeout: 1200,
|
|
796
|
-
});
|
|
797
|
-
if (!res || res.status !== 0) return null;
|
|
798
|
-
const out = String(res.stdout || '');
|
|
799
|
-
for (const line of out.split('\\n')) {
|
|
800
|
-
if (!line || line[0] !== 'n') continue;
|
|
801
|
-
const name = line.slice(1);
|
|
802
|
-
if (!name.startsWith(SESSIONS_ROOT + path.sep)) continue;
|
|
803
|
-
const m = name.match(/([0-9a-f-]{36})\\.(jsonl|settings\\.json)$/i);
|
|
804
|
-
if (!m) continue;
|
|
805
|
-
const id = m[1];
|
|
806
|
-
const workspaceDir = path.dirname(name);
|
|
807
|
-
if (path.dirname(workspaceDir) !== SESSIONS_ROOT) continue;
|
|
808
|
-
return { workspaceDir, id };
|
|
809
|
-
}
|
|
810
|
-
} catch {
|
|
811
|
-
return null;
|
|
812
|
-
}
|
|
813
|
-
return null;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
async function resolveSessionFromProcessGroup(shouldAbort, maxTries = 20) {
|
|
817
|
-
if (!isPositiveInt(PGID)) return null;
|
|
818
|
-
// Wait a little for droid to create/open the session files.
|
|
819
|
-
for (let i = 0; i < maxTries; i++) {
|
|
820
|
-
if (shouldAbort && shouldAbort()) return null;
|
|
821
|
-
const pids = listPidsInProcessGroup(PGID);
|
|
822
|
-
const found = resolveOpenSessionFromPids(pids);
|
|
823
|
-
if (found) return found;
|
|
824
|
-
await sleep(100);
|
|
825
|
-
}
|
|
826
|
-
return null;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function safeReadFile(filePath) {
|
|
830
|
-
try {
|
|
831
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
832
|
-
} catch {
|
|
833
|
-
return null;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function safeJsonParse(text) {
|
|
838
|
-
if (!text) return null;
|
|
839
|
-
try {
|
|
840
|
-
// Factory settings/config files can contain comments. Strip them safely without
|
|
841
|
-
// breaking URLs like "http://..." which contain "//" inside strings.
|
|
842
|
-
const stripComments = (input) => {
|
|
843
|
-
let out = '';
|
|
844
|
-
let inString = false;
|
|
845
|
-
let escape = false;
|
|
846
|
-
for (let i = 0; i < input.length; i++) {
|
|
847
|
-
const ch = input[i];
|
|
848
|
-
const next = input[i + 1];
|
|
849
|
-
|
|
850
|
-
if (inString) {
|
|
851
|
-
out += ch;
|
|
852
|
-
if (escape) {
|
|
853
|
-
escape = false;
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
if (ch === '\\\\') {
|
|
857
|
-
escape = true;
|
|
858
|
-
continue;
|
|
859
|
-
}
|
|
860
|
-
if (ch === '"') {
|
|
861
|
-
inString = false;
|
|
862
|
-
}
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
if (ch === '"') {
|
|
867
|
-
inString = true;
|
|
868
|
-
out += ch;
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// Line comment
|
|
873
|
-
if (ch === '/' && next === '/') {
|
|
874
|
-
while (i < input.length && input[i] !== '\\n') i++;
|
|
875
|
-
out += '\\n';
|
|
876
|
-
continue;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// Block comment
|
|
880
|
-
if (ch === '/' && next === '*') {
|
|
881
|
-
i += 2;
|
|
882
|
-
while (i < input.length && !(input[i] === '*' && input[i + 1] === '/')) i++;
|
|
883
|
-
i += 1;
|
|
884
|
-
continue;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
out += ch;
|
|
888
|
-
}
|
|
889
|
-
return out;
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
return JSON.parse(stripComments(text));
|
|
893
|
-
} catch {
|
|
894
|
-
return null;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
function readJsonFile(filePath) {
|
|
899
|
-
return safeJsonParse(safeReadFile(filePath));
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
function isUuid(text) {
|
|
903
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(text);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function parseResume(args) {
|
|
907
|
-
for (let i = 0; i < args.length; i++) {
|
|
908
|
-
const a = args[i];
|
|
909
|
-
if (a === '-r' || a === '--resume') {
|
|
910
|
-
const next = args[i + 1];
|
|
911
|
-
if (next && isUuid(next)) return { resumeFlag: true, resumeId: next };
|
|
912
|
-
return { resumeFlag: true, resumeId: null };
|
|
913
|
-
}
|
|
914
|
-
if (a.startsWith('--resume=')) {
|
|
915
|
-
const value = a.slice('--resume='.length);
|
|
916
|
-
return { resumeFlag: true, resumeId: isUuid(value) ? value : null };
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
return { resumeFlag: false, resumeId: null };
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
function sanitizeWorkspaceDirName(cwd) {
|
|
923
|
-
return String(cwd)
|
|
924
|
-
.replace(/[:]/g, '')
|
|
925
|
-
.replace(/[\\\\/]/g, '-')
|
|
926
|
-
.replace(/\\s+/g, '-');
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
function listSessionCandidates(workspaceDir) {
|
|
930
|
-
let files = [];
|
|
931
|
-
try {
|
|
932
|
-
files = fs.readdirSync(workspaceDir);
|
|
933
|
-
} catch {
|
|
934
|
-
return [];
|
|
935
|
-
}
|
|
936
|
-
const candidates = [];
|
|
937
|
-
for (const file of files) {
|
|
938
|
-
const m = file.match(/^([0-9a-f-]{36})\\.(jsonl|settings\\.json)$/i);
|
|
939
|
-
if (!m) continue;
|
|
940
|
-
const id = m[1];
|
|
941
|
-
const fullPath = path.join(workspaceDir, file);
|
|
942
|
-
try {
|
|
943
|
-
const stat = fs.statSync(fullPath);
|
|
944
|
-
candidates.push({ id, fullPath, mtimeMs: stat.mtimeMs });
|
|
945
|
-
} catch {
|
|
946
|
-
// ignore
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
return candidates;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
function findWorkspaceDirForSessionId(workspaceDirs, sessionId) {
|
|
953
|
-
for (const dir of workspaceDirs) {
|
|
954
|
-
try {
|
|
955
|
-
const settingsPath = path.join(dir, sessionId + '.settings.json');
|
|
956
|
-
if (fs.existsSync(settingsPath)) return dir;
|
|
957
|
-
} catch {
|
|
958
|
-
// ignore
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
return null;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function pickLatestSessionAcross(workspaceDirs) {
|
|
965
|
-
let best = null;
|
|
966
|
-
for (const dir of workspaceDirs) {
|
|
967
|
-
const candidates = listSessionCandidates(dir);
|
|
968
|
-
for (const c of candidates) {
|
|
969
|
-
if (!best || c.mtimeMs > best.mtimeMs) {
|
|
970
|
-
best = { workspaceDir: dir, id: c.id, mtimeMs: c.mtimeMs };
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
return best ? { workspaceDir: best.workspaceDir, id: best.id } : null;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
async function waitForNewSessionAcross(workspaceDirs, knownIdsByWorkspace, startMs, shouldAbort) {
|
|
978
|
-
for (let i = 0; i < 80; i++) {
|
|
979
|
-
if (shouldAbort && shouldAbort()) return null;
|
|
980
|
-
let best = null;
|
|
981
|
-
for (const dir of workspaceDirs) {
|
|
982
|
-
const known = knownIdsByWorkspace.get(dir) || new Set();
|
|
983
|
-
const candidates = listSessionCandidates(dir);
|
|
984
|
-
for (const c of candidates) {
|
|
985
|
-
if (!(c.mtimeMs >= startMs - 50 || !known.has(c.id))) continue;
|
|
986
|
-
if (!best || c.mtimeMs > best.mtimeMs) {
|
|
987
|
-
best = { workspaceDir: dir, id: c.id, mtimeMs: c.mtimeMs };
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
if (best?.id) return { workspaceDir: best.workspaceDir, id: best.id };
|
|
992
|
-
await sleep(100);
|
|
993
|
-
}
|
|
994
|
-
return null;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
function safeRealpath(p) {
|
|
998
|
-
try {
|
|
999
|
-
return fs.realpathSync(p);
|
|
1000
|
-
} catch {
|
|
1001
|
-
return null;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
function resolveWorkspaceDirs(cwd) {
|
|
1006
|
-
const logical = cwd;
|
|
1007
|
-
const real = safeRealpath(cwd);
|
|
1008
|
-
const dirs = [];
|
|
1009
|
-
for (const value of [logical, real]) {
|
|
1010
|
-
if (!value || typeof value !== 'string') continue;
|
|
1011
|
-
dirs.push(path.join(SESSIONS_ROOT, sanitizeWorkspaceDirName(value)));
|
|
1012
|
-
}
|
|
1013
|
-
return Array.from(new Set(dirs));
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
function resolveSessionSettings(workspaceDir, sessionId) {
|
|
1017
|
-
const settingsPath = path.join(workspaceDir, sessionId + '.settings.json');
|
|
1018
|
-
const settings = readJsonFile(settingsPath) || {};
|
|
1019
|
-
return { settingsPath, settings };
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
function resolveGlobalSettingsModel() {
|
|
1023
|
-
const global = readJsonFile(GLOBAL_SETTINGS_PATH);
|
|
1024
|
-
return global && typeof global.model === 'string' ? global.model : null;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
function resolveCustomModelIndex(modelId) {
|
|
1028
|
-
if (typeof modelId !== 'string') return null;
|
|
1029
|
-
if (!modelId.startsWith('custom:')) return null;
|
|
1030
|
-
const m = modelId.match(/-(\\d+)$/);
|
|
1031
|
-
if (!m) return null;
|
|
1032
|
-
const idx = Number(m[1]);
|
|
1033
|
-
return Number.isFinite(idx) ? idx : null;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
function resolveUnderlyingModelId(modelId, factoryConfig) {
|
|
1037
|
-
const idx = resolveCustomModelIndex(modelId);
|
|
1038
|
-
if (idx == null) return modelId;
|
|
1039
|
-
const entry = factoryConfig?.custom_models?.[idx];
|
|
1040
|
-
if (entry && typeof entry.model === 'string') return entry.model;
|
|
1041
|
-
return modelId;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
function resolveProvider(modelId, factoryConfig) {
|
|
1045
|
-
const idx = resolveCustomModelIndex(modelId);
|
|
1046
|
-
if (idx != null) {
|
|
1047
|
-
const entry = factoryConfig?.custom_models?.[idx];
|
|
1048
|
-
if (entry && typeof entry.provider === 'string') return entry.provider;
|
|
1049
|
-
}
|
|
1050
|
-
if (typeof modelId === 'string' && modelId.startsWith('claude-')) return 'anthropic';
|
|
1051
|
-
return '';
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
function formatInt(n) {
|
|
1055
|
-
if (!Number.isFinite(n)) return '0';
|
|
1056
|
-
return Math.round(n).toString();
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
function formatTokens(n) {
|
|
1060
|
-
if (!Number.isFinite(n)) return '0';
|
|
1061
|
-
const sign = n < 0 ? '-' : '';
|
|
1062
|
-
const abs = Math.abs(n);
|
|
1063
|
-
if (abs >= 1_000_000) {
|
|
1064
|
-
const v = abs / 1_000_000;
|
|
1065
|
-
const s = v >= 10 ? v.toFixed(0) : v.toFixed(1);
|
|
1066
|
-
return sign + s.replace(/\\.0$/, '') + 'M';
|
|
1067
|
-
}
|
|
1068
|
-
if (abs >= 10_000) {
|
|
1069
|
-
const v = abs / 1_000;
|
|
1070
|
-
const s = v >= 100 ? v.toFixed(0) : v.toFixed(1);
|
|
1071
|
-
return sign + s.replace(/\\.0$/, '') + 'k';
|
|
1072
|
-
}
|
|
1073
|
-
return sign + Math.round(abs).toString();
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function emitFrame(line) {
|
|
1077
|
-
try {
|
|
1078
|
-
process.stdout.write(String(line || '') + '\\n');
|
|
1079
|
-
} catch {
|
|
1080
|
-
// ignore
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
function seg(bg, fg, text) {
|
|
1085
|
-
if (!text) return '';
|
|
1086
|
-
return '\\x1b[48;5;' + bg + 'm' + '\\x1b[38;5;' + fg + 'm' + ' ' + text + ' ' + '\\x1b[0m';
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
function resolveGitBranch(cwd) {
|
|
1090
|
-
try {
|
|
1091
|
-
const res = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
1092
|
-
cwd,
|
|
1093
|
-
encoding: 'utf8',
|
|
1094
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1095
|
-
timeout: 800,
|
|
1096
|
-
});
|
|
1097
|
-
if (res && res.status === 0) {
|
|
1098
|
-
const branch = String(res.stdout || '').trim();
|
|
1099
|
-
if (branch && branch !== 'HEAD') return branch;
|
|
1100
|
-
}
|
|
1101
|
-
} catch {}
|
|
1102
|
-
try {
|
|
1103
|
-
const headPath = path.join(cwd, '.git', 'HEAD');
|
|
1104
|
-
const head = safeReadFile(headPath);
|
|
1105
|
-
if (head && head.startsWith('ref: ')) {
|
|
1106
|
-
const ref = head.slice('ref: '.length).trim();
|
|
1107
|
-
const m = ref.match(/refs\\/heads\\/(.+)$/);
|
|
1108
|
-
if (m) return m[1];
|
|
1109
|
-
}
|
|
1110
|
-
} catch {}
|
|
1111
|
-
return '';
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
function resolveGitDiffSummary(cwd) {
|
|
1115
|
-
try {
|
|
1116
|
-
const res = spawnSync('git', ['diff', '--shortstat'], {
|
|
1117
|
-
cwd,
|
|
1118
|
-
encoding: 'utf8',
|
|
1119
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1120
|
-
timeout: 800,
|
|
1121
|
-
});
|
|
1122
|
-
if (!res || res.status !== 0) return '';
|
|
1123
|
-
const text = String(res.stdout || '').trim();
|
|
1124
|
-
if (!text) return '';
|
|
1125
|
-
const ins = (text.match(/(\\d+)\\sinsertions?\\(\\+\\)/) || [])[1];
|
|
1126
|
-
const del = (text.match(/(\\d+)\\sdeletions?\\(-\\)/) || [])[1];
|
|
1127
|
-
const i = ins ? Number(ins) : 0;
|
|
1128
|
-
const d = del ? Number(del) : 0;
|
|
1129
|
-
if (!Number.isFinite(i) && !Number.isFinite(d)) return '';
|
|
1130
|
-
if (i === 0 && d === 0) return '';
|
|
1131
|
-
return '(+' + formatInt(i) + ',-' + formatInt(d) + ')';
|
|
1132
|
-
} catch {
|
|
1133
|
-
return '';
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
function buildLine(params) {
|
|
1138
|
-
const {
|
|
1139
|
-
provider,
|
|
1140
|
-
model,
|
|
1141
|
-
cwdBase,
|
|
1142
|
-
gitBranch,
|
|
1143
|
-
gitDiff,
|
|
1144
|
-
usedTokens,
|
|
1145
|
-
cacheRead,
|
|
1146
|
-
deltaInput,
|
|
1147
|
-
lastOutputTokens,
|
|
1148
|
-
sessionUsage,
|
|
1149
|
-
compacting,
|
|
1150
|
-
ctxAvailable,
|
|
1151
|
-
ctxApprox,
|
|
1152
|
-
ctxOverflow,
|
|
1153
|
-
} = params;
|
|
1154
|
-
|
|
1155
|
-
const ctxValue = !ctxAvailable
|
|
1156
|
-
? '--'
|
|
1157
|
-
: (ctxApprox ? '~' : '') + formatTokens(usedTokens) + (ctxOverflow ? '+' : '');
|
|
1158
|
-
let ctxPart = 'Ctx: ' + ctxValue;
|
|
1159
|
-
|
|
1160
|
-
const cachePart =
|
|
1161
|
-
ctxAvailable && !ctxApprox && !ctxOverflow && (cacheRead > 0 || deltaInput > 0)
|
|
1162
|
-
? ' c' + formatTokens(cacheRead) + '+n' + formatTokens(deltaInput)
|
|
1163
|
-
: '';
|
|
1164
|
-
|
|
1165
|
-
const compactPart = compacting ? ' COMPACT' : '';
|
|
1166
|
-
|
|
1167
|
-
const usagePart = (() => {
|
|
1168
|
-
const u = sessionUsage || {};
|
|
1169
|
-
const input = Number(u.inputTokens ?? 0);
|
|
1170
|
-
const output = Number(u.outputTokens ?? 0);
|
|
1171
|
-
const cacheCreation = Number(u.cacheCreationTokens ?? 0);
|
|
1172
|
-
const cacheReadTotal = Number(u.cacheReadTokens ?? 0);
|
|
1173
|
-
const thinking = Number(u.thinkingTokens ?? 0);
|
|
1174
|
-
if (!(input || output || cacheCreation || cacheReadTotal || thinking)) return '';
|
|
1175
|
-
const parts = [];
|
|
1176
|
-
if (input) parts.push('In:' + formatTokens(input));
|
|
1177
|
-
if (output) parts.push('Out:' + formatTokens(output));
|
|
1178
|
-
if (cacheCreation) parts.push('Cre:' + formatTokens(cacheCreation));
|
|
1179
|
-
if (cacheReadTotal) parts.push('Read:' + formatTokens(cacheReadTotal));
|
|
1180
|
-
if (thinking) parts.push('Think:' + formatTokens(thinking));
|
|
1181
|
-
if (lastOutputTokens > 0) parts.push('LastOut:' + formatTokens(lastOutputTokens));
|
|
1182
|
-
return parts.join(' ');
|
|
1183
|
-
})();
|
|
1184
|
-
|
|
1185
|
-
const modelPart = model ? 'Model: ' + model : '';
|
|
1186
|
-
const providerPart = provider ? 'Prov: ' + provider : '';
|
|
1187
|
-
const cwdPart = cwdBase ? 'cwd: ' + cwdBase : '';
|
|
1188
|
-
const branchPart = gitBranch ? '\\uE0A0 ' + gitBranch : '';
|
|
1189
|
-
const diffPart = gitDiff || '';
|
|
1190
|
-
|
|
1191
|
-
// Background segments (powerline-like blocks)
|
|
1192
|
-
const sModel = seg(88, 15, modelPart); // dark red
|
|
1193
|
-
const sProvider = seg(160, 15, providerPart); // red
|
|
1194
|
-
const sCtx = seg(220, 0, ctxPart + (cachePart ? ' (' + cachePart.trim() + ')' : '')); // yellow
|
|
1195
|
-
const sUsage = seg(173, 0, usagePart); // orange-ish
|
|
1196
|
-
const sBranch = seg(24, 15, branchPart); // blue
|
|
1197
|
-
const sDiff = seg(34, 0, diffPart); // green
|
|
1198
|
-
const sCwd = seg(238, 15, cwdPart); // gray
|
|
1199
|
-
const sExtra = seg(99, 15, compactPart.trim()); // purple-ish
|
|
1200
|
-
|
|
1201
|
-
return [sModel, sProvider, sCtx, sUsage, sBranch, sDiff, sCwd, sExtra].filter(Boolean).join('');
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
async function main() {
|
|
1205
|
-
let factoryConfig = readJsonFile(CONFIG_PATH) || {};
|
|
1206
|
-
|
|
1207
|
-
const cwd = process.cwd();
|
|
1208
|
-
const cwdBase = path.basename(cwd) || cwd;
|
|
1209
|
-
const workspaceDirs = resolveWorkspaceDirs(cwd);
|
|
1210
|
-
const knownIdsByWorkspace = new Map();
|
|
1211
|
-
for (const dir of workspaceDirs) {
|
|
1212
|
-
const set = new Set();
|
|
1213
|
-
for (const c of listSessionCandidates(dir)) set.add(c.id);
|
|
1214
|
-
knownIdsByWorkspace.set(dir, set);
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
const { resumeFlag, resumeId } = parseResume(ARGS);
|
|
1218
|
-
|
|
1219
|
-
let sessionId = null;
|
|
1220
|
-
let workspaceDir = null;
|
|
1221
|
-
if (resumeId) {
|
|
1222
|
-
sessionId = resumeId;
|
|
1223
|
-
workspaceDir = findWorkspaceDirForSessionId(workspaceDirs, sessionId) || workspaceDirs[0] || null;
|
|
1224
|
-
} else {
|
|
1225
|
-
let abortResolve = false;
|
|
1226
|
-
const shouldAbort = () => abortResolve;
|
|
1227
|
-
|
|
1228
|
-
const byProcPromise = resolveSessionFromProcessGroup(shouldAbort, 20);
|
|
1229
|
-
|
|
1230
|
-
let picked = null;
|
|
1231
|
-
if (resumeFlag) {
|
|
1232
|
-
// For --resume without an explicit id, don't block startup too long on ps/lsof.
|
|
1233
|
-
// Prefer process-group resolution when it is fast; otherwise fall back to latest.
|
|
1234
|
-
picked = await Promise.race([
|
|
1235
|
-
byProcPromise,
|
|
1236
|
-
sleep(400).then(() => null),
|
|
1237
|
-
]);
|
|
1238
|
-
if (!picked) picked = pickLatestSessionAcross(workspaceDirs);
|
|
1239
|
-
} else {
|
|
1240
|
-
const freshPromise = waitForNewSessionAcross(workspaceDirs, knownIdsByWorkspace, START_MS, shouldAbort);
|
|
1241
|
-
picked = await firstNonNull([byProcPromise, freshPromise]);
|
|
1242
|
-
if (!picked) picked = pickLatestSessionAcross(workspaceDirs);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
abortResolve = true;
|
|
1246
|
-
|
|
1247
|
-
sessionId = picked?.id || null;
|
|
1248
|
-
workspaceDir = picked?.workspaceDir || workspaceDirs[0] || null;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
if (!sessionId || !workspaceDir) return;
|
|
1252
|
-
let sessionIdLower = String(sessionId).toLowerCase();
|
|
1253
|
-
|
|
1254
|
-
let settingsPath = '';
|
|
1255
|
-
let sessionSettings = {};
|
|
1256
|
-
({ settingsPath, settings: sessionSettings } = resolveSessionSettings(workspaceDir, sessionId));
|
|
1257
|
-
|
|
1258
|
-
let configMtimeMs = safeStatMtimeMs(CONFIG_PATH);
|
|
1259
|
-
let globalSettingsMtimeMs = safeStatMtimeMs(GLOBAL_SETTINGS_PATH);
|
|
1260
|
-
let globalSettingsModel = resolveGlobalSettingsModel();
|
|
1261
|
-
|
|
1262
|
-
let modelIdFromLog = null;
|
|
1263
|
-
|
|
1264
|
-
function resolveActiveModelId() {
|
|
1265
|
-
const fromSession =
|
|
1266
|
-
sessionSettings && typeof sessionSettings.model === 'string' ? sessionSettings.model : null;
|
|
1267
|
-
if (fromSession && String(fromSession).startsWith('custom:')) return fromSession;
|
|
1268
|
-
const fromLog = typeof modelIdFromLog === 'string' ? modelIdFromLog : null;
|
|
1269
|
-
if (fromLog) return fromLog;
|
|
1270
|
-
return fromSession || globalSettingsModel || null;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
let modelId = resolveActiveModelId();
|
|
1274
|
-
|
|
1275
|
-
let provider =
|
|
1276
|
-
sessionSettings && typeof sessionSettings.providerLock === 'string'
|
|
1277
|
-
? sessionSettings.providerLock
|
|
1278
|
-
: resolveProvider(modelId, factoryConfig);
|
|
1279
|
-
let underlyingModel = resolveUnderlyingModelId(modelId, factoryConfig) || modelId || 'unknown';
|
|
1280
|
-
|
|
1281
|
-
function refreshModel() {
|
|
1282
|
-
const nextModelId = resolveActiveModelId();
|
|
1283
|
-
|
|
1284
|
-
// Use providerLock if set, otherwise resolve from model/config (same logic as initialization)
|
|
1285
|
-
const nextProvider =
|
|
1286
|
-
sessionSettings && typeof sessionSettings.providerLock === 'string'
|
|
1287
|
-
? sessionSettings.providerLock
|
|
1288
|
-
: resolveProvider(nextModelId, factoryConfig);
|
|
1289
|
-
const nextUnderlying = resolveUnderlyingModelId(nextModelId, factoryConfig) || nextModelId || 'unknown';
|
|
1290
|
-
|
|
1291
|
-
let changed = false;
|
|
1292
|
-
if (nextModelId !== modelId) {
|
|
1293
|
-
modelId = nextModelId;
|
|
1294
|
-
changed = true;
|
|
1295
|
-
}
|
|
1296
|
-
if (nextProvider !== provider) {
|
|
1297
|
-
provider = nextProvider;
|
|
1298
|
-
changed = true;
|
|
1299
|
-
}
|
|
1300
|
-
if (nextUnderlying !== underlyingModel) {
|
|
1301
|
-
underlyingModel = nextUnderlying;
|
|
1302
|
-
changed = true;
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
if (changed) renderNow();
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
let last = { cacheReadInputTokens: 0, contextCount: 0, outputTokens: 0 };
|
|
1309
|
-
let sessionUsage =
|
|
1310
|
-
sessionSettings && typeof sessionSettings.tokenUsage === 'object' && sessionSettings.tokenUsage
|
|
1311
|
-
? sessionSettings.tokenUsage
|
|
1312
|
-
: {};
|
|
1313
|
-
let compacting = false;
|
|
1314
|
-
let lastRenderAt = 0;
|
|
1315
|
-
let lastRenderedLine = '';
|
|
1316
|
-
let gitBranch = '';
|
|
1317
|
-
let gitDiff = '';
|
|
1318
|
-
let lastContextMs = 0;
|
|
1319
|
-
let ctxAvailable = false;
|
|
1320
|
-
let ctxApprox = false;
|
|
1321
|
-
let ctxOverflow = false;
|
|
1322
|
-
let ctxOverrideUsedTokens = null;
|
|
1323
|
-
|
|
1324
|
-
let baselineCacheReadInputTokens = 0;
|
|
1325
|
-
let knownContextMaxTokens = 0;
|
|
1326
|
-
let pendingCompactionSuffixTokens = null;
|
|
1327
|
-
let pendingCompactionSummaryOutputTokens = null;
|
|
1328
|
-
let pendingCompactionSummaryTsMs = null;
|
|
1329
|
-
|
|
1330
|
-
function renderNow() {
|
|
1331
|
-
const override = Number.isFinite(ctxOverrideUsedTokens) && ctxOverrideUsedTokens > 0 ? ctxOverrideUsedTokens : null;
|
|
1332
|
-
const usedTokens = override != null ? override : (last.cacheReadInputTokens || 0) + (last.contextCount || 0);
|
|
1333
|
-
const cacheRead = override != null ? 0 : last.cacheReadInputTokens || 0;
|
|
1334
|
-
const deltaInput = override != null ? 0 : last.contextCount || 0;
|
|
1335
|
-
const line = buildLine({
|
|
1336
|
-
provider,
|
|
1337
|
-
model: underlyingModel,
|
|
1338
|
-
cwdBase,
|
|
1339
|
-
gitBranch,
|
|
1340
|
-
gitDiff,
|
|
1341
|
-
usedTokens,
|
|
1342
|
-
cacheRead,
|
|
1343
|
-
deltaInput,
|
|
1344
|
-
lastOutputTokens: last.outputTokens || 0,
|
|
1345
|
-
sessionUsage,
|
|
1346
|
-
compacting,
|
|
1347
|
-
ctxAvailable: override != null ? true : ctxAvailable,
|
|
1348
|
-
ctxApprox,
|
|
1349
|
-
ctxOverflow,
|
|
1350
|
-
});
|
|
1351
|
-
if (line !== lastRenderedLine) {
|
|
1352
|
-
lastRenderedLine = line;
|
|
1353
|
-
emitFrame(line);
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// Initial render.
|
|
1358
|
-
renderNow();
|
|
1359
|
-
|
|
1360
|
-
// Resolve git info asynchronously so startup isn't blocked on large repos.
|
|
1361
|
-
setTimeout(() => {
|
|
1362
|
-
try {
|
|
1363
|
-
gitBranch = resolveGitBranch(cwd);
|
|
1364
|
-
gitDiff = resolveGitDiffSummary(cwd);
|
|
1365
|
-
renderNow();
|
|
1366
|
-
} catch {}
|
|
1367
|
-
}, 0).unref();
|
|
1368
|
-
|
|
1369
|
-
// Seed known context max tokens from recent log failures (some providers omit explicit counts).
|
|
1370
|
-
setTimeout(() => {
|
|
1371
|
-
try {
|
|
1372
|
-
seedKnownContextMaxTokensFromLog(8 * 1024 * 1024);
|
|
1373
|
-
} catch {}
|
|
1374
|
-
}, 0).unref();
|
|
1375
|
-
|
|
1376
|
-
let reseedInProgress = false;
|
|
1377
|
-
let reseedQueued = false;
|
|
1378
|
-
|
|
1379
|
-
function extractModelIdFromContext(ctx) {
|
|
1380
|
-
const tagged = ctx?.tags?.modelId;
|
|
1381
|
-
if (typeof tagged === 'string') return tagged;
|
|
1382
|
-
const direct = ctx?.modelId;
|
|
1383
|
-
return typeof direct === 'string' ? direct : null;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
function updateLastFromContext(ctx, updateOutputTokens, tsMs) {
|
|
1387
|
-
const ts = Number.isFinite(tsMs) ? tsMs : null;
|
|
1388
|
-
if (ts != null && lastContextMs && ts < lastContextMs) return false;
|
|
1389
|
-
const cacheRead = Number(ctx?.cacheReadInputTokens);
|
|
1390
|
-
const contextCount = Number(ctx?.contextCount);
|
|
1391
|
-
const out = Number(ctx?.outputTokens);
|
|
1392
|
-
const hasTokens =
|
|
1393
|
-
(Number.isFinite(cacheRead) && cacheRead > 0) ||
|
|
1394
|
-
(Number.isFinite(contextCount) && contextCount > 0);
|
|
1395
|
-
if (hasTokens) {
|
|
1396
|
-
// Treat 0/0 as "not reported" (some providers log zeros even when prompt exists).
|
|
1397
|
-
// If at least one field is >0, accept both fields (including zero) as reliable.
|
|
1398
|
-
if (Number.isFinite(cacheRead)) last.cacheReadInputTokens = cacheRead;
|
|
1399
|
-
if (Number.isFinite(contextCount)) last.contextCount = contextCount;
|
|
1400
|
-
ctxAvailable = true;
|
|
1401
|
-
ctxOverrideUsedTokens = null;
|
|
1402
|
-
ctxApprox = false;
|
|
1403
|
-
ctxOverflow = false;
|
|
1404
|
-
if (Number.isFinite(cacheRead) && cacheRead > 0) {
|
|
1405
|
-
baselineCacheReadInputTokens = baselineCacheReadInputTokens
|
|
1406
|
-
? Math.min(baselineCacheReadInputTokens, cacheRead)
|
|
1407
|
-
: cacheRead;
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
if (updateOutputTokens && Number.isFinite(out)) last.outputTokens = out;
|
|
1411
|
-
if (hasTokens && ts != null) lastContextMs = ts;
|
|
1412
|
-
|
|
1413
|
-
const nextModelIdFromLog = extractModelIdFromContext(ctx);
|
|
1414
|
-
if (nextModelIdFromLog && nextModelIdFromLog !== modelIdFromLog) {
|
|
1415
|
-
modelIdFromLog = nextModelIdFromLog;
|
|
1416
|
-
refreshModel();
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
return true;
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
function setCtxOverride(usedTokens, options) {
|
|
1423
|
-
const opts = options || {};
|
|
1424
|
-
const v = Number(usedTokens);
|
|
1425
|
-
if (!Number.isFinite(v) || v <= 0) return false;
|
|
1426
|
-
const prevUsed = ctxOverrideUsedTokens;
|
|
1427
|
-
const prevApprox = ctxApprox;
|
|
1428
|
-
const prevOverflow = ctxOverflow;
|
|
1429
|
-
ctxOverrideUsedTokens = v;
|
|
1430
|
-
ctxAvailable = true;
|
|
1431
|
-
ctxApprox = !!opts.approx;
|
|
1432
|
-
ctxOverflow = !!opts.overflow;
|
|
1433
|
-
const ts = Number.isFinite(opts.tsMs) ? opts.tsMs : null;
|
|
1434
|
-
if (ts != null && (!lastContextMs || ts > lastContextMs)) lastContextMs = ts;
|
|
1435
|
-
if (prevUsed !== ctxOverrideUsedTokens || prevApprox !== ctxApprox || prevOverflow !== ctxOverflow) {
|
|
1436
|
-
renderNow();
|
|
1437
|
-
}
|
|
1438
|
-
return true;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
function parseContextLimitFromMessage(message) {
|
|
1442
|
-
const s = String(message || '');
|
|
1443
|
-
let promptTokens = null;
|
|
1444
|
-
let maxTokens = null;
|
|
1445
|
-
|
|
1446
|
-
const pair = s.match(/(\\d+)\\s*tokens\\s*>\\s*(\\d+)\\s*maximum/i);
|
|
1447
|
-
if (pair) {
|
|
1448
|
-
const prompt = Number(pair[1]);
|
|
1449
|
-
const max = Number(pair[2]);
|
|
1450
|
-
if (Number.isFinite(prompt)) promptTokens = prompt;
|
|
1451
|
-
if (Number.isFinite(max)) maxTokens = max;
|
|
1452
|
-
return { promptTokens, maxTokens };
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
const promptMatch = s.match(/prompt\\s+is\\s+too\\s+long:\\s*(\\d+)\\s*tokens/i);
|
|
1456
|
-
if (promptMatch) {
|
|
1457
|
-
const prompt = Number(promptMatch[1]);
|
|
1458
|
-
if (Number.isFinite(prompt)) promptTokens = prompt;
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
const maxMatch = s.match(/>\\s*(\\d+)\\s*maximum/i);
|
|
1462
|
-
if (maxMatch) {
|
|
1463
|
-
const max = Number(maxMatch[1]);
|
|
1464
|
-
if (Number.isFinite(max)) maxTokens = max;
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
return { promptTokens, maxTokens };
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
function seedKnownContextMaxTokensFromLog(maxScanBytes = 4 * 1024 * 1024) {
|
|
1471
|
-
try {
|
|
1472
|
-
const stat = fs.statSync(LOG_PATH);
|
|
1473
|
-
const size = Number(stat?.size ?? 0);
|
|
1474
|
-
if (!(size > 0)) return;
|
|
1475
|
-
|
|
1476
|
-
const scan = Math.max(256 * 1024, maxScanBytes);
|
|
1477
|
-
const readSize = Math.min(size, scan);
|
|
1478
|
-
const start = Math.max(0, size - readSize);
|
|
1479
|
-
|
|
1480
|
-
const buf = Buffer.alloc(readSize);
|
|
1481
|
-
const fd = fs.openSync(LOG_PATH, 'r');
|
|
1482
|
-
try {
|
|
1483
|
-
fs.readSync(fd, buf, 0, readSize, start);
|
|
1484
|
-
} finally {
|
|
1485
|
-
try {
|
|
1486
|
-
fs.closeSync(fd);
|
|
1487
|
-
} catch {}
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
const text = buf.toString('utf8');
|
|
1491
|
-
const re = /(\\d+)\\s*tokens\\s*>\\s*(\\d+)\\s*maximum/gi;
|
|
1492
|
-
let m;
|
|
1493
|
-
while ((m = re.exec(text))) {
|
|
1494
|
-
const max = Number(m[2]);
|
|
1495
|
-
if (Number.isFinite(max) && max > 0) knownContextMaxTokens = max;
|
|
1496
|
-
}
|
|
1497
|
-
} catch {}
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
function maybeUpdateCtxFromContextLimitFailure(line, ctx, tsMs) {
|
|
1501
|
-
if (!line || !ctx) return false;
|
|
1502
|
-
if (!String(line).includes('[Chat route failure]')) return false;
|
|
1503
|
-
const reason = ctx?.reason;
|
|
1504
|
-
if (reason !== 'llmContextExceeded') return false;
|
|
1505
|
-
|
|
1506
|
-
const msg = ctx?.error?.message;
|
|
1507
|
-
if (typeof msg !== 'string' || !msg) return false;
|
|
1508
|
-
const parsed = parseContextLimitFromMessage(msg);
|
|
1509
|
-
const max = Number(parsed?.maxTokens);
|
|
1510
|
-
if (Number.isFinite(max) && max > 0) knownContextMaxTokens = max;
|
|
1511
|
-
|
|
1512
|
-
const prompt = Number(parsed?.promptTokens);
|
|
1513
|
-
if (Number.isFinite(prompt) && prompt > 0) {
|
|
1514
|
-
return setCtxOverride(prompt, { tsMs, approx: false, overflow: false });
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
if (Number.isFinite(max) && max > 0) {
|
|
1518
|
-
return setCtxOverride(max, { tsMs, approx: false, overflow: true });
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
if (knownContextMaxTokens > 0) {
|
|
1522
|
-
return setCtxOverride(knownContextMaxTokens, { tsMs, approx: false, overflow: true });
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
return false;
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
function maybeCaptureCompactionSuffix(line, ctx) {
|
|
1529
|
-
if (!line || !ctx) return;
|
|
1530
|
-
if (!String(line).includes('[Compaction] Suffix selection')) return;
|
|
1531
|
-
const suffix = Number(ctx?.suffixTokens);
|
|
1532
|
-
if (Number.isFinite(suffix) && suffix >= 0) pendingCompactionSuffixTokens = suffix;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
function maybeApplyPostCompactionEstimate(line, ctx, tsMs) {
|
|
1536
|
-
if (!line || !ctx) return false;
|
|
1537
|
-
if (!String(line).includes('[Compaction] End')) return false;
|
|
1538
|
-
if (ctx?.eventType !== 'compaction' || ctx?.state !== 'end') return false;
|
|
1539
|
-
const reason = ctx?.reason || ctx?.tags?.compactionReason || null;
|
|
1540
|
-
if (!(reason === 'context_limit' || reason === 'manual')) return false;
|
|
1541
|
-
|
|
1542
|
-
const summaryOut = Number(ctx?.summaryOutputTokens);
|
|
1543
|
-
if (!Number.isFinite(summaryOut) || summaryOut < 0) return false;
|
|
1544
|
-
|
|
1545
|
-
const prefix = baselineCacheReadInputTokens > 0 ? baselineCacheReadInputTokens : 0;
|
|
1546
|
-
const suffix = Number.isFinite(pendingCompactionSuffixTokens) ? pendingCompactionSuffixTokens : 0;
|
|
1547
|
-
pendingCompactionSuffixTokens = null;
|
|
1548
|
-
|
|
1549
|
-
const est = prefix + suffix + summaryOut;
|
|
1550
|
-
if (est <= 0) return false;
|
|
1551
|
-
return setCtxOverride(est, { tsMs, approx: true, overflow: false });
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
function maybeApplyCompactSessionEstimate(sessionIdToEstimate, options, attempt = 0) {
|
|
1555
|
-
const opts = options && typeof options === 'object' ? options : {};
|
|
1556
|
-
const id = String(sessionIdToEstimate || '');
|
|
1557
|
-
if (!isUuid(id)) return;
|
|
1558
|
-
if (!workspaceDir) return;
|
|
1559
|
-
|
|
1560
|
-
// forceApply allows overriding even if ctx is already set (useful for manual /compress)
|
|
1561
|
-
const forceApply = !!opts?.forceApply;
|
|
1562
|
-
if (!forceApply) {
|
|
1563
|
-
if (ctxAvailable) return;
|
|
1564
|
-
if (Number.isFinite(ctxOverrideUsedTokens) && ctxOverrideUsedTokens > 0) return;
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
const suffixVal = opts?.suffixTokens;
|
|
1568
|
-
const suffixTokens =
|
|
1569
|
-
typeof suffixVal === 'number' && Number.isFinite(suffixVal) && suffixVal >= 0 ? suffixVal : 0;
|
|
1570
|
-
const tsVal = opts?.tsMs;
|
|
1571
|
-
const ts = typeof tsVal === 'number' && Number.isFinite(tsVal) ? tsVal : null;
|
|
1572
|
-
|
|
1573
|
-
const jsonlPath = path.join(workspaceDir, id + '.jsonl');
|
|
1574
|
-
let head = null;
|
|
1575
|
-
try {
|
|
1576
|
-
const fd = fs.openSync(jsonlPath, 'r');
|
|
1577
|
-
try {
|
|
1578
|
-
const maxBytes = 2 * 1024 * 1024;
|
|
1579
|
-
const buf = Buffer.alloc(maxBytes);
|
|
1580
|
-
const bytes = fs.readSync(fd, buf, 0, maxBytes, 0);
|
|
1581
|
-
head = buf.slice(0, Math.max(0, bytes)).toString('utf8');
|
|
1582
|
-
} finally {
|
|
1583
|
-
try {
|
|
1584
|
-
fs.closeSync(fd);
|
|
1585
|
-
} catch {}
|
|
1586
|
-
}
|
|
1587
|
-
} catch {
|
|
1588
|
-
head = null;
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
if (!head) {
|
|
1592
|
-
if (attempt < 40) {
|
|
1593
|
-
setTimeout(() => {
|
|
1594
|
-
maybeApplyCompactSessionEstimate(id, opts, attempt + 1);
|
|
1595
|
-
}, 150).unref();
|
|
1596
|
-
}
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
let summaryText = null;
|
|
1601
|
-
for (const raw of head.split('\\n')) {
|
|
1602
|
-
if (!raw) continue;
|
|
1603
|
-
let obj;
|
|
1604
|
-
try {
|
|
1605
|
-
obj = JSON.parse(raw);
|
|
1606
|
-
} catch {
|
|
1607
|
-
continue;
|
|
1608
|
-
}
|
|
1609
|
-
if (obj && obj.type === 'compaction_state' && typeof obj.summaryText === 'string') {
|
|
1610
|
-
summaryText = obj.summaryText;
|
|
1611
|
-
break;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
if (!summaryText) {
|
|
1615
|
-
if (attempt < 40) {
|
|
1616
|
-
setTimeout(() => {
|
|
1617
|
-
maybeApplyCompactSessionEstimate(id, opts, attempt + 1);
|
|
1618
|
-
}, 150).unref();
|
|
1619
|
-
}
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
// Rough token estimate (no tokenizer deps): English-like text averages ~4 chars/token.
|
|
1624
|
-
// Non-ASCII tends to be denser; use a smaller divisor.
|
|
1625
|
-
let ascii = 0;
|
|
1626
|
-
let other = 0;
|
|
1627
|
-
for (let i = 0; i < summaryText.length; i++) {
|
|
1628
|
-
const code = summaryText.charCodeAt(i);
|
|
1629
|
-
if (code <= 0x7f) ascii += 1;
|
|
1630
|
-
else other += 1;
|
|
1631
|
-
}
|
|
1632
|
-
const summaryTokens = Math.max(1, Math.ceil(ascii / 4 + other / 1.5));
|
|
1633
|
-
const prefix = baselineCacheReadInputTokens > 0 ? baselineCacheReadInputTokens : 0;
|
|
1634
|
-
const est = prefix + suffixTokens + summaryTokens;
|
|
1635
|
-
if (est > 0) setCtxOverride(est, { tsMs: ts, approx: true, overflow: false });
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
function seedLastContextFromLog(options) {
|
|
1639
|
-
const opts = options || {};
|
|
1640
|
-
const maxScanBytes = Number.isFinite(opts.maxScanBytes) ? opts.maxScanBytes : 64 * 1024 * 1024;
|
|
1641
|
-
const preferStreaming = !!opts.preferStreaming;
|
|
1642
|
-
const minTimestampMs = Number.isFinite(lastContextMs) && lastContextMs > 0 ? lastContextMs : 0;
|
|
1643
|
-
const earlyStopAfterBestBytes = Math.min(2 * 1024 * 1024, Math.max(256 * 1024, maxScanBytes));
|
|
1644
|
-
|
|
1645
|
-
if (reseedInProgress) {
|
|
1646
|
-
reseedQueued = true;
|
|
1647
|
-
return;
|
|
1648
|
-
}
|
|
1649
|
-
reseedInProgress = true;
|
|
1650
|
-
|
|
1651
|
-
setTimeout(() => {
|
|
1652
|
-
try {
|
|
1653
|
-
// Backward scan to find the most recent context entry for this session.
|
|
1654
|
-
// Prefer streaming context if requested; otherwise accept any context line
|
|
1655
|
-
// that includes cacheReadInputTokens/contextCount fields.
|
|
1656
|
-
const CHUNK_BYTES = 1024 * 1024; // 1 MiB
|
|
1657
|
-
|
|
1658
|
-
const fd = fs.openSync(LOG_PATH, 'r');
|
|
1659
|
-
try {
|
|
1660
|
-
const stat = fs.fstatSync(fd);
|
|
1661
|
-
const size = Number(stat?.size ?? 0);
|
|
1662
|
-
let pos = size;
|
|
1663
|
-
let scanned = 0;
|
|
1664
|
-
let remainder = '';
|
|
1665
|
-
let bestCtx = null;
|
|
1666
|
-
let bestIsStreaming = false;
|
|
1667
|
-
let bestTs = null;
|
|
1668
|
-
let bestHasTs = false;
|
|
1669
|
-
let bytesSinceBest = 0;
|
|
1670
|
-
|
|
1671
|
-
while (pos > 0 && scanned < maxScanBytes && (!bestHasTs || bytesSinceBest < earlyStopAfterBestBytes)) {
|
|
1672
|
-
const readSize = Math.min(CHUNK_BYTES, pos);
|
|
1673
|
-
const start = pos - readSize;
|
|
1674
|
-
const buf = Buffer.alloc(readSize);
|
|
1675
|
-
fs.readSync(fd, buf, 0, readSize, start);
|
|
1676
|
-
pos = start;
|
|
1677
|
-
scanned += readSize;
|
|
1678
|
-
bytesSinceBest += readSize;
|
|
1679
|
-
|
|
1680
|
-
let text = buf.toString('utf8') + remainder;
|
|
1681
|
-
let lines = String(text).split('\\n');
|
|
1682
|
-
remainder = lines.shift() || '';
|
|
1683
|
-
if (pos === 0 && remainder) {
|
|
1684
|
-
lines.unshift(remainder);
|
|
1685
|
-
remainder = '';
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1689
|
-
const line = String(lines[i] || '').trimEnd();
|
|
1690
|
-
if (!line) continue;
|
|
1691
|
-
if (!line.includes('Context:')) continue;
|
|
1692
|
-
const sid = extractSessionIdFromLine(line);
|
|
1693
|
-
if (!sid || String(sid).toLowerCase() !== sessionIdLower) continue;
|
|
1694
|
-
|
|
1695
|
-
const isStreaming = line.includes('[Agent] Streaming result');
|
|
1696
|
-
if (preferStreaming && !isStreaming) continue;
|
|
1697
|
-
|
|
1698
|
-
const ctxIndex = line.indexOf('Context: ');
|
|
1699
|
-
if (ctxIndex === -1) continue;
|
|
1700
|
-
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
1701
|
-
let ctx;
|
|
1702
|
-
try {
|
|
1703
|
-
ctx = JSON.parse(jsonStr);
|
|
1704
|
-
} catch {
|
|
1705
|
-
continue;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
const cacheRead = Number(ctx?.cacheReadInputTokens);
|
|
1709
|
-
const contextCount = Number(ctx?.contextCount);
|
|
1710
|
-
const hasUsage = Number.isFinite(cacheRead) || Number.isFinite(contextCount);
|
|
1711
|
-
if (!hasUsage) continue;
|
|
1712
|
-
|
|
1713
|
-
const ts = parseLineTimestampMs(line);
|
|
1714
|
-
if (ts != null && minTimestampMs && ts < minTimestampMs) {
|
|
1715
|
-
continue;
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
if (ts != null) {
|
|
1719
|
-
if (!bestHasTs || ts > bestTs) {
|
|
1720
|
-
bestCtx = ctx;
|
|
1721
|
-
bestIsStreaming = isStreaming;
|
|
1722
|
-
bestTs = ts;
|
|
1723
|
-
bestHasTs = true;
|
|
1724
|
-
bytesSinceBest = 0;
|
|
1725
|
-
}
|
|
1726
|
-
} else if (!bestHasTs && !bestCtx) {
|
|
1727
|
-
// No timestamps available yet: the first match when scanning backward
|
|
1728
|
-
// is the most recent in file order.
|
|
1729
|
-
bestCtx = ctx;
|
|
1730
|
-
bestIsStreaming = isStreaming;
|
|
1731
|
-
bestTs = null;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
if (remainder.length > 8192) remainder = remainder.slice(-8192);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
if (bestCtx) {
|
|
1739
|
-
updateLastFromContext(bestCtx, bestIsStreaming, bestTs);
|
|
1740
|
-
}
|
|
1741
|
-
} finally {
|
|
1742
|
-
try {
|
|
1743
|
-
fs.closeSync(fd);
|
|
1744
|
-
} catch {}
|
|
1745
|
-
}
|
|
1746
|
-
} catch {
|
|
1747
|
-
// ignore
|
|
1748
|
-
} finally {
|
|
1749
|
-
reseedInProgress = false;
|
|
1750
|
-
if (reseedQueued) {
|
|
1751
|
-
reseedQueued = false;
|
|
1752
|
-
seedLastContextFromLog({ maxScanBytes, preferStreaming });
|
|
1753
|
-
return;
|
|
1754
|
-
}
|
|
1755
|
-
renderNow();
|
|
1756
|
-
}
|
|
1757
|
-
}, 0).unref();
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
// Seed prompt-context usage from existing logs (important for resumed sessions).
|
|
1761
|
-
// Do this asynchronously to avoid delaying the first statusline frame.
|
|
1762
|
-
let initialSeedDone = false;
|
|
1763
|
-
if (resumeFlag || resumeId) {
|
|
1764
|
-
initialSeedDone = true;
|
|
1765
|
-
seedLastContextFromLog({ maxScanBytes: 64 * 1024 * 1024, preferStreaming: true });
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
// Watch session settings for autonomy/reasoning changes (cheap polling with mtime).
|
|
1769
|
-
let settingsMtimeMs = 0;
|
|
1770
|
-
let lastCtxPollMs = 0;
|
|
1771
|
-
setInterval(() => {
|
|
1772
|
-
// Refresh config/global settings if they changed (model display depends on these).
|
|
1773
|
-
const configMtime = safeStatMtimeMs(CONFIG_PATH);
|
|
1774
|
-
if (configMtime && configMtime !== configMtimeMs) {
|
|
1775
|
-
configMtimeMs = configMtime;
|
|
1776
|
-
factoryConfig = readJsonFile(CONFIG_PATH) || {};
|
|
1777
|
-
refreshModel();
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
const globalMtime = safeStatMtimeMs(GLOBAL_SETTINGS_PATH);
|
|
1781
|
-
if (globalMtime && globalMtime !== globalSettingsMtimeMs) {
|
|
1782
|
-
globalSettingsMtimeMs = globalMtime;
|
|
1783
|
-
globalSettingsModel = resolveGlobalSettingsModel();
|
|
1784
|
-
refreshModel();
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
try {
|
|
1788
|
-
const stat = fs.statSync(settingsPath);
|
|
1789
|
-
if (stat.mtimeMs === settingsMtimeMs) return;
|
|
1790
|
-
settingsMtimeMs = stat.mtimeMs;
|
|
1791
|
-
const next = readJsonFile(settingsPath) || {};
|
|
1792
|
-
sessionSettings = next;
|
|
1793
|
-
|
|
1794
|
-
// Keep session token usage in sync (used by /status).
|
|
1795
|
-
if (next && typeof next.tokenUsage === 'object' && next.tokenUsage) {
|
|
1796
|
-
sessionUsage = next.tokenUsage;
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
// Keep model/provider in sync (model can change during a running session).
|
|
1800
|
-
refreshModel();
|
|
1801
|
-
|
|
1802
|
-
const now = Date.now();
|
|
1803
|
-
if (now - lastRenderAt >= MIN_RENDER_INTERVAL_MS) {
|
|
1804
|
-
lastRenderAt = now;
|
|
1805
|
-
renderNow();
|
|
1806
|
-
}
|
|
1807
|
-
} catch {
|
|
1808
|
-
// ignore
|
|
1809
|
-
}
|
|
1810
|
-
}, 750).unref();
|
|
1811
|
-
|
|
1812
|
-
// Fallback: periodically rescan log if context is still zero after startup.
|
|
1813
|
-
// This handles cases where tail misses early log entries.
|
|
1814
|
-
setInterval(() => {
|
|
1815
|
-
const now = Date.now();
|
|
1816
|
-
if (now - START_MS < 3000) return; // wait 3s after startup
|
|
1817
|
-
if (last.contextCount > 0 || last.cacheReadInputTokens > 0) return; // already have data
|
|
1818
|
-
if (now - lastCtxPollMs < 5000) return; // throttle to every 5s
|
|
1819
|
-
lastCtxPollMs = now;
|
|
1820
|
-
seedLastContextFromLog({ maxScanBytes: 4 * 1024 * 1024, preferStreaming: false });
|
|
1821
|
-
}, 2000).unref();
|
|
1822
|
-
|
|
1823
|
-
function switchToSession(nextSessionId) {
|
|
1824
|
-
if (!nextSessionId || !isUuid(nextSessionId)) return;
|
|
1825
|
-
const nextLower = String(nextSessionId).toLowerCase();
|
|
1826
|
-
if (nextLower === sessionIdLower) return;
|
|
1827
|
-
|
|
1828
|
-
sessionId = nextSessionId;
|
|
1829
|
-
sessionIdLower = nextLower;
|
|
1830
|
-
|
|
1831
|
-
const resolved = resolveSessionSettings(workspaceDir, nextSessionId);
|
|
1832
|
-
settingsPath = resolved.settingsPath;
|
|
1833
|
-
sessionSettings = resolved.settings || {};
|
|
1834
|
-
|
|
1835
|
-
sessionUsage =
|
|
1836
|
-
sessionSettings && typeof sessionSettings.tokenUsage === 'object' && sessionSettings.tokenUsage
|
|
1837
|
-
? sessionSettings.tokenUsage
|
|
1838
|
-
: {};
|
|
1839
|
-
|
|
1840
|
-
// Reset cached state for the new session.
|
|
1841
|
-
last = { cacheReadInputTokens: 0, contextCount: 0, outputTokens: 0 };
|
|
1842
|
-
lastContextMs = 0;
|
|
1843
|
-
ctxAvailable = false;
|
|
1844
|
-
ctxApprox = false;
|
|
1845
|
-
ctxOverflow = false;
|
|
1846
|
-
ctxOverrideUsedTokens = null;
|
|
1847
|
-
pendingCompactionSuffixTokens = null;
|
|
1848
|
-
pendingCompactionSummaryOutputTokens = null;
|
|
1849
|
-
pendingCompactionSummaryTsMs = null;
|
|
1850
|
-
modelIdFromLog = null;
|
|
1851
|
-
compacting = false;
|
|
1852
|
-
settingsMtimeMs = 0;
|
|
1853
|
-
lastCtxPollMs = 0;
|
|
1854
|
-
|
|
1855
|
-
refreshModel();
|
|
1856
|
-
renderNow();
|
|
1857
|
-
|
|
1858
|
-
// Best-effort: if the new session already has Context lines in the log, seed quickly.
|
|
1859
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
// Follow the Factory log and update based on session-scoped events.
|
|
1863
|
-
const tail = spawn('tail', ['-n', '0', '-F', LOG_PATH], {
|
|
1864
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1865
|
-
});
|
|
1866
|
-
|
|
1867
|
-
let buffer = '';
|
|
1868
|
-
tail.stdout.on('data', (chunk) => {
|
|
1869
|
-
buffer += String(chunk);
|
|
1870
|
-
while (true) {
|
|
1871
|
-
const idx = buffer.indexOf('\\n');
|
|
1872
|
-
if (idx === -1) break;
|
|
1873
|
-
const line = buffer.slice(0, idx).trimEnd();
|
|
1874
|
-
buffer = buffer.slice(idx + 1);
|
|
1875
|
-
|
|
1876
|
-
const tsMs = parseLineTimestampMs(line);
|
|
1877
|
-
const lineSessionId = extractSessionIdFromLine(line);
|
|
1878
|
-
const isSessionLine =
|
|
1879
|
-
lineSessionId && String(lineSessionId).toLowerCase() === sessionIdLower;
|
|
1880
|
-
|
|
1881
|
-
if (compacting && line.includes('[Compaction] End') && line.includes('Context:')) {
|
|
1882
|
-
const ctxIndex = line.indexOf('Context: ');
|
|
1883
|
-
if (ctxIndex !== -1) {
|
|
1884
|
-
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
1885
|
-
try {
|
|
1886
|
-
const meta = JSON.parse(jsonStr);
|
|
1887
|
-
const summaryOut = Number(meta?.summaryOutputTokens);
|
|
1888
|
-
if (
|
|
1889
|
-
meta?.eventType === 'compaction' &&
|
|
1890
|
-
meta?.state === 'end' &&
|
|
1891
|
-
Number.isFinite(summaryOut) &&
|
|
1892
|
-
summaryOut >= 0
|
|
1893
|
-
) {
|
|
1894
|
-
pendingCompactionSummaryOutputTokens = summaryOut;
|
|
1895
|
-
if (tsMs != null) pendingCompactionSummaryTsMs = tsMs;
|
|
1896
|
-
}
|
|
1897
|
-
} catch {
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
// /compress (aka /compact) can create a new session ID. Follow it so ctx/model keep updating.
|
|
1903
|
-
if (line.includes('oldSessionId') && line.includes('newSessionId') && line.includes('Context:')) {
|
|
1904
|
-
const ctxIndex = line.indexOf('Context: ');
|
|
1905
|
-
if (ctxIndex !== -1) {
|
|
1906
|
-
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
1907
|
-
try {
|
|
1908
|
-
const meta = JSON.parse(jsonStr);
|
|
1909
|
-
const oldId = meta?.oldSessionId;
|
|
1910
|
-
const newId = meta?.newSessionId;
|
|
1911
|
-
if (
|
|
1912
|
-
isUuid(oldId) &&
|
|
1913
|
-
isUuid(newId) &&
|
|
1914
|
-
String(oldId).toLowerCase() === sessionIdLower &&
|
|
1915
|
-
String(newId).toLowerCase() !== sessionIdLower
|
|
1916
|
-
) {
|
|
1917
|
-
const suffixTokens = Number.isFinite(pendingCompactionSuffixTokens)
|
|
1918
|
-
? pendingCompactionSuffixTokens
|
|
1919
|
-
: 0;
|
|
1920
|
-
const summaryOutTokens = Number.isFinite(pendingCompactionSummaryOutputTokens)
|
|
1921
|
-
? pendingCompactionSummaryOutputTokens
|
|
1922
|
-
: null;
|
|
1923
|
-
const summaryTsMs = Number.isFinite(pendingCompactionSummaryTsMs) ? pendingCompactionSummaryTsMs : null;
|
|
1924
|
-
|
|
1925
|
-
// Save baseline before switching session (it persists across sessions)
|
|
1926
|
-
const savedBaseline = baselineCacheReadInputTokens;
|
|
1927
|
-
|
|
1928
|
-
switchToSession(String(newId));
|
|
1929
|
-
|
|
1930
|
-
// For manual /compress, immediately set an estimated ctx value
|
|
1931
|
-
// This ensures the statusline shows a reasonable value right after compression
|
|
1932
|
-
if (summaryOutTokens != null && summaryOutTokens > 0) {
|
|
1933
|
-
const prefix = savedBaseline > 0 ? savedBaseline : 0;
|
|
1934
|
-
const est = prefix + suffixTokens + summaryOutTokens;
|
|
1935
|
-
if (est > 0) {
|
|
1936
|
-
setCtxOverride(est, { tsMs: summaryTsMs != null ? summaryTsMs : tsMs, approx: true, overflow: false });
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
// Always attempt to get a more accurate estimate from the new session's jsonl
|
|
1941
|
-
// This will read the compaction_state and estimate tokens from summaryText
|
|
1942
|
-
// Note: we pass forceApply=true to override even if ctxOverrideUsedTokens is set,
|
|
1943
|
-
// because the jsonl-based estimate may be more accurate
|
|
1944
|
-
maybeApplyCompactSessionEstimate(String(newId), {
|
|
1945
|
-
suffixTokens,
|
|
1946
|
-
tsMs: summaryTsMs != null ? summaryTsMs : tsMs,
|
|
1947
|
-
forceApply: true,
|
|
1948
|
-
});
|
|
1949
|
-
continue;
|
|
1950
|
-
}
|
|
1951
|
-
} catch {
|
|
1952
|
-
// ignore
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
let compactionChanged = false;
|
|
1958
|
-
let compactionEnded = false;
|
|
1959
|
-
if (line.includes('[Compaction]')) {
|
|
1960
|
-
// Accept session-scoped compaction lines; allow end markers to clear even
|
|
1961
|
-
// if the line lacks a session id (some builds omit Context on end lines).
|
|
1962
|
-
// For manual /compress, [Compaction] End uses the NEW session ID, so we need
|
|
1963
|
-
// to also accept End markers when compacting is true and it's an End line.
|
|
1964
|
-
const isManualCompactionEnd = compacting &&
|
|
1965
|
-
line.includes('[Compaction] End') &&
|
|
1966
|
-
lineSessionId &&
|
|
1967
|
-
String(lineSessionId).toLowerCase() !== sessionIdLower;
|
|
1968
|
-
if (isSessionLine || (compacting && !lineSessionId) || isManualCompactionEnd) {
|
|
1969
|
-
const next = nextCompactionState(line, compacting);
|
|
1970
|
-
if (next !== compacting) {
|
|
1971
|
-
compacting = next;
|
|
1972
|
-
compactionChanged = true;
|
|
1973
|
-
if (!compacting) compactionEnded = true;
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
if (compactionChanged && compacting) {
|
|
1979
|
-
pendingCompactionSuffixTokens = null;
|
|
1980
|
-
pendingCompactionSummaryOutputTokens = null;
|
|
1981
|
-
pendingCompactionSummaryTsMs = null;
|
|
1982
|
-
// Compaction can start after a context-limit error. Ensure we display the latest
|
|
1983
|
-
// pre-compaction ctx by reseeding from log (tail can miss bursts).
|
|
1984
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: true });
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
if (compactionEnded) {
|
|
1988
|
-
// ctx usage changes dramatically after compaction, but the next Context line
|
|
1989
|
-
// can be delayed. Clear displayed ctx immediately to avoid showing stale numbers.
|
|
1990
|
-
last.cacheReadInputTokens = 0;
|
|
1991
|
-
last.contextCount = 0;
|
|
1992
|
-
ctxAvailable = false;
|
|
1993
|
-
ctxOverrideUsedTokens = null;
|
|
1994
|
-
ctxApprox = false;
|
|
1995
|
-
ctxOverflow = false;
|
|
1996
|
-
if (tsMs != null) lastContextMs = tsMs;
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
if (!line.includes('Context:')) {
|
|
2000
|
-
if (compactionChanged) {
|
|
2001
|
-
lastRenderAt = Date.now();
|
|
2002
|
-
renderNow();
|
|
2003
|
-
}
|
|
2004
|
-
if (compactionEnded) {
|
|
2005
|
-
// Compaction often completes between turns. Refresh ctx numbers promptly
|
|
2006
|
-
// by rescanning the most recent Context entry for this session.
|
|
2007
|
-
setTimeout(() => {
|
|
2008
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
2009
|
-
}, 250).unref();
|
|
2010
|
-
}
|
|
2011
|
-
continue;
|
|
2012
|
-
}
|
|
2013
|
-
if (!isSessionLine) {
|
|
2014
|
-
if (compactionChanged) {
|
|
2015
|
-
lastRenderAt = Date.now();
|
|
2016
|
-
renderNow();
|
|
2017
|
-
}
|
|
2018
|
-
if (compactionEnded) {
|
|
2019
|
-
setTimeout(() => {
|
|
2020
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
2021
|
-
}, 250).unref();
|
|
2022
|
-
}
|
|
2023
|
-
continue;
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
const ctxIndex = line.indexOf('Context: ');
|
|
2027
|
-
if (ctxIndex === -1) continue;
|
|
2028
|
-
const jsonStr = line.slice(ctxIndex + 'Context: '.length).trim();
|
|
2029
|
-
let ctx;
|
|
2030
|
-
try {
|
|
2031
|
-
ctx = JSON.parse(jsonStr);
|
|
2032
|
-
} catch {
|
|
2033
|
-
if (compactionChanged) {
|
|
2034
|
-
lastRenderAt = Date.now();
|
|
2035
|
-
renderNow();
|
|
2036
|
-
}
|
|
2037
|
-
continue;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
// Context usage can appear on multiple session-scoped log lines; update whenever present.
|
|
2041
|
-
// (Streaming is still the best source for outputTokens / LastOut.)
|
|
2042
|
-
updateLastFromContext(ctx, false, tsMs);
|
|
2043
|
-
|
|
2044
|
-
maybeCaptureCompactionSuffix(line, ctx);
|
|
2045
|
-
maybeUpdateCtxFromContextLimitFailure(line, ctx, tsMs);
|
|
2046
|
-
maybeApplyPostCompactionEstimate(line, ctx, tsMs);
|
|
2047
|
-
|
|
2048
|
-
// For new sessions: if this is the first valid Context line and ctx is still 0,
|
|
2049
|
-
// trigger a reseed to catch any earlier log entries we might have missed.
|
|
2050
|
-
if (!initialSeedDone && last.contextCount === 0) {
|
|
2051
|
-
initialSeedDone = true;
|
|
2052
|
-
setTimeout(() => {
|
|
2053
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
2054
|
-
}, 100).unref();
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
if (line.includes('[Agent] Streaming result')) {
|
|
2058
|
-
updateLastFromContext(ctx, true, tsMs);
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
const now = Date.now();
|
|
2062
|
-
if (compactionChanged || now - lastRenderAt >= MIN_RENDER_INTERVAL_MS) {
|
|
2063
|
-
lastRenderAt = now;
|
|
2064
|
-
renderNow();
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
if (compactionEnded) {
|
|
2068
|
-
setTimeout(() => {
|
|
2069
|
-
seedLastContextFromLog({ maxScanBytes: 8 * 1024 * 1024, preferStreaming: false });
|
|
2070
|
-
}, 250).unref();
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
});
|
|
2074
|
-
|
|
2075
|
-
const stop = () => {
|
|
2076
|
-
try { tail.kill('SIGTERM'); } catch {}
|
|
2077
|
-
process.exit(0);
|
|
2078
|
-
};
|
|
2079
|
-
|
|
2080
|
-
process.on('SIGTERM', stop);
|
|
2081
|
-
process.on('SIGINT', stop);
|
|
2082
|
-
process.on('SIGHUP', stop);
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
main().catch(() => {});
|
|
2086
|
-
`;
|
|
2087
|
-
}
|
|
2088
|
-
function generateStatuslineWrapperScript(execTargetPath, monitorScriptPath, sessionsScriptPath) {
|
|
2089
|
-
return generateStatuslineWrapperScriptBun(execTargetPath, monitorScriptPath, sessionsScriptPath);
|
|
2090
|
-
}
|
|
2091
|
-
function generateStatuslineWrapperScriptBun(execTargetPath, monitorScriptPath, sessionsScriptPath) {
|
|
2092
|
-
return `#!/usr/bin/env bun
|
|
2093
|
-
// Droid with Statusline (Bun PTY proxy)
|
|
2094
|
-
// Auto-generated by droid-patch --statusline
|
|
2095
|
-
|
|
2096
|
-
const EXEC_TARGET = ${JSON.stringify(execTargetPath)};
|
|
2097
|
-
const STATUSLINE_MONITOR = ${JSON.stringify(monitorScriptPath)};
|
|
2098
|
-
const SESSIONS_SCRIPT = ${sessionsScriptPath ? JSON.stringify(sessionsScriptPath) : "null"};
|
|
2099
|
-
|
|
2100
|
-
const IS_APPLE_TERMINAL = process.env.TERM_PROGRAM === "Apple_Terminal";
|
|
2101
|
-
const MIN_RENDER_INTERVAL_MS = IS_APPLE_TERMINAL ? 800 : 400;
|
|
2102
|
-
const QUIET_MS = 50;
|
|
2103
|
-
const FORCE_REPAINT_INTERVAL_MS = 2000;
|
|
2104
|
-
const RESERVED_ROWS = 1;
|
|
2105
|
-
|
|
2106
|
-
const BYPASS_FLAGS = new Set(["--help", "-h", "--version", "-V"]);
|
|
2107
|
-
const BYPASS_COMMANDS = new Set(["help", "version", "completion", "completions", "exec"]);
|
|
2108
|
-
|
|
2109
|
-
function shouldPassthrough(argv) {
|
|
2110
|
-
for (const a of argv) {
|
|
2111
|
-
if (a === "--") break;
|
|
2112
|
-
if (BYPASS_FLAGS.has(a)) return true;
|
|
2113
|
-
}
|
|
2114
|
-
let endOpts = false;
|
|
2115
|
-
let cmd = null;
|
|
2116
|
-
for (const a of argv) {
|
|
2117
|
-
if (a === "--") {
|
|
2118
|
-
endOpts = true;
|
|
2119
|
-
continue;
|
|
2120
|
-
}
|
|
2121
|
-
if (!endOpts && a.startsWith("-")) continue;
|
|
2122
|
-
cmd = a;
|
|
2123
|
-
break;
|
|
2124
|
-
}
|
|
2125
|
-
return cmd && BYPASS_COMMANDS.has(cmd);
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
function isSessionsCommand(argv) {
|
|
2129
|
-
for (const a of argv) {
|
|
2130
|
-
if (a === "--") return false;
|
|
2131
|
-
if (a === "--sessions") return true;
|
|
2132
|
-
}
|
|
2133
|
-
return false;
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
async function execPassthrough(argv) {
|
|
2137
|
-
const proc = Bun.spawn([EXEC_TARGET, ...argv], {
|
|
2138
|
-
stdin: "inherit",
|
|
2139
|
-
stdout: "inherit",
|
|
2140
|
-
stderr: "inherit",
|
|
2141
|
-
});
|
|
2142
|
-
const code = await proc.exited;
|
|
2143
|
-
process.exit(code ?? 0);
|
|
2144
|
-
}
|
|
2145
|
-
|
|
2146
|
-
async function runSessions() {
|
|
2147
|
-
if (SESSIONS_SCRIPT) {
|
|
2148
|
-
const proc = Bun.spawn(["node", String(SESSIONS_SCRIPT)], {
|
|
2149
|
-
stdin: "inherit",
|
|
2150
|
-
stdout: "inherit",
|
|
2151
|
-
stderr: "inherit",
|
|
2152
|
-
});
|
|
2153
|
-
const code = await proc.exited;
|
|
2154
|
-
process.exit(code ?? 0);
|
|
2155
|
-
}
|
|
2156
|
-
process.stderr.write("[statusline] sessions script not found\\n");
|
|
2157
|
-
process.exit(1);
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
function writeStdout(s) {
|
|
2161
|
-
try {
|
|
2162
|
-
process.stdout.write(s);
|
|
2163
|
-
} catch {
|
|
2164
|
-
// ignore
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
function termSize() {
|
|
2169
|
-
const rows = Number(process.stdout.rows || 24);
|
|
2170
|
-
const cols = Number(process.stdout.columns || 80);
|
|
2171
|
-
return { rows: Number.isFinite(rows) ? rows : 24, cols: Number.isFinite(cols) ? cols : 80 };
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
const ANSI_RE = /\\x1b\\[[0-9;]*m/g;
|
|
2175
|
-
const RESET_SGR = "\\x1b[0m";
|
|
2176
|
-
|
|
2177
|
-
function visibleWidth(text) {
|
|
2178
|
-
return String(text || "").replace(ANSI_RE, "").length;
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
function clampAnsi(text, cols) {
|
|
2182
|
-
if (!cols || cols <= 0) return String(text || "");
|
|
2183
|
-
cols = cols > 1 ? cols - 1 : cols; // avoid last-column wrap
|
|
2184
|
-
if (cols < 10) return String(text || "");
|
|
2185
|
-
const s = String(text || "");
|
|
2186
|
-
let visible = 0;
|
|
2187
|
-
let i = 0;
|
|
2188
|
-
const out = [];
|
|
2189
|
-
while (i < s.length) {
|
|
2190
|
-
const ch = s[i];
|
|
2191
|
-
if (ch === "\\x1b") {
|
|
2192
|
-
const m = s.indexOf("m", i);
|
|
2193
|
-
if (m !== -1) {
|
|
2194
|
-
out.push(s.slice(i, m + 1));
|
|
2195
|
-
i = m + 1;
|
|
2196
|
-
continue;
|
|
2197
|
-
}
|
|
2198
|
-
out.push(ch);
|
|
2199
|
-
i += 1;
|
|
2200
|
-
continue;
|
|
2201
|
-
}
|
|
2202
|
-
if (visible >= cols) break;
|
|
2203
|
-
out.push(ch);
|
|
2204
|
-
i += 1;
|
|
2205
|
-
visible += 1;
|
|
2206
|
-
}
|
|
2207
|
-
if (i < s.length && cols >= 1) {
|
|
2208
|
-
if (visible >= cols) {
|
|
2209
|
-
if (out.length) out[out.length - 1] = "…";
|
|
2210
|
-
else out.push("…");
|
|
2211
|
-
} else {
|
|
2212
|
-
out.push("…");
|
|
2213
|
-
}
|
|
2214
|
-
out.push(RESET_SGR);
|
|
2215
|
-
}
|
|
2216
|
-
return out.join("");
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
function splitSegments(text) {
|
|
2220
|
-
if (!text) return [];
|
|
2221
|
-
const s = String(text);
|
|
2222
|
-
const segments = [];
|
|
2223
|
-
let start = 0;
|
|
2224
|
-
while (true) {
|
|
2225
|
-
const idx = s.indexOf(RESET_SGR, start);
|
|
2226
|
-
if (idx === -1) {
|
|
2227
|
-
const tail = s.slice(start);
|
|
2228
|
-
if (tail) segments.push(tail);
|
|
2229
|
-
break;
|
|
2230
|
-
}
|
|
2231
|
-
const seg = s.slice(start, idx + RESET_SGR.length);
|
|
2232
|
-
if (seg) segments.push(seg);
|
|
2233
|
-
start = idx + RESET_SGR.length;
|
|
2234
|
-
}
|
|
2235
|
-
return segments;
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
function wrapSegments(segments, cols) {
|
|
2239
|
-
if (!segments || segments.length === 0) return [""];
|
|
2240
|
-
if (!cols || cols <= 0) return [segments.join("")];
|
|
2241
|
-
|
|
2242
|
-
const lines = [];
|
|
2243
|
-
let cur = [];
|
|
2244
|
-
let curW = 0;
|
|
2245
|
-
|
|
2246
|
-
for (let seg of segments) {
|
|
2247
|
-
let segW = visibleWidth(seg);
|
|
2248
|
-
if (segW <= 0) continue;
|
|
2249
|
-
|
|
2250
|
-
if (cur.length === 0) {
|
|
2251
|
-
if (segW > cols) {
|
|
2252
|
-
seg = clampAnsi(seg, cols);
|
|
2253
|
-
segW = visibleWidth(seg);
|
|
2254
|
-
}
|
|
2255
|
-
cur = [seg];
|
|
2256
|
-
curW = segW;
|
|
2257
|
-
continue;
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
if (curW + segW <= cols) {
|
|
2261
|
-
cur.push(seg);
|
|
2262
|
-
curW += segW;
|
|
2263
|
-
} else {
|
|
2264
|
-
lines.push(cur.join(""));
|
|
2265
|
-
if (segW > cols) {
|
|
2266
|
-
seg = clampAnsi(seg, cols);
|
|
2267
|
-
segW = visibleWidth(seg);
|
|
2268
|
-
}
|
|
2269
|
-
cur = [seg];
|
|
2270
|
-
curW = segW;
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
if (cur.length) lines.push(cur.join(""));
|
|
2275
|
-
return lines.length ? lines : [""];
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
class StatusRenderer {
|
|
2279
|
-
constructor() {
|
|
2280
|
-
this.raw = "";
|
|
2281
|
-
this.segments = [];
|
|
2282
|
-
this.lines = [""];
|
|
2283
|
-
this.activeReservedRows = RESERVED_ROWS;
|
|
2284
|
-
this.force = false;
|
|
2285
|
-
this.urgent = false;
|
|
2286
|
-
this.lastRenderMs = 0;
|
|
2287
|
-
this.lastChildOutMs = 0;
|
|
2288
|
-
this.cursorVisible = true;
|
|
2289
|
-
}
|
|
2290
|
-
noteChildOutput() {
|
|
2291
|
-
this.lastChildOutMs = Date.now();
|
|
2292
|
-
}
|
|
2293
|
-
setCursorVisible(v) {
|
|
2294
|
-
this.cursorVisible = !!v;
|
|
2295
|
-
}
|
|
2296
|
-
forceRepaint(urgent = false) {
|
|
2297
|
-
this.force = true;
|
|
2298
|
-
if (urgent) this.urgent = true;
|
|
2299
|
-
}
|
|
2300
|
-
setActiveReservedRows(n) {
|
|
2301
|
-
const v = Number(n || 1);
|
|
2302
|
-
this.activeReservedRows = Number.isFinite(v) ? Math.max(1, Math.trunc(v)) : 1;
|
|
2303
|
-
}
|
|
2304
|
-
setLine(line) {
|
|
2305
|
-
const next = String(line || "");
|
|
2306
|
-
if (next !== this.raw) {
|
|
2307
|
-
this.raw = next;
|
|
2308
|
-
this.segments = splitSegments(next);
|
|
2309
|
-
this.force = true;
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
desiredReservedRows(physicalRows, cols, minReserved) {
|
|
2313
|
-
let rows = Number(physicalRows || 24);
|
|
2314
|
-
rows = Number.isFinite(rows) ? rows : 24;
|
|
2315
|
-
cols = Number(cols || 80);
|
|
2316
|
-
cols = Number.isFinite(cols) ? cols : 80;
|
|
2317
|
-
|
|
2318
|
-
const maxReserved = Math.max(1, rows - 4);
|
|
2319
|
-
const segs = this.segments.length ? this.segments : (this.raw ? [this.raw] : []);
|
|
2320
|
-
let lines = segs.length ? wrapSegments(segs, cols) : [""];
|
|
2321
|
-
|
|
2322
|
-
const needed = Math.min(lines.length, maxReserved);
|
|
2323
|
-
let desired = Math.max(Number(minReserved || 1), needed);
|
|
2324
|
-
desired = Math.min(desired, maxReserved);
|
|
2325
|
-
|
|
2326
|
-
if (lines.length < desired) lines = new Array(desired - lines.length).fill("").concat(lines);
|
|
2327
|
-
if (lines.length > desired) lines = lines.slice(-desired);
|
|
2328
|
-
|
|
2329
|
-
this.lines = lines;
|
|
2330
|
-
return desired;
|
|
2331
|
-
}
|
|
2332
|
-
clearReservedArea(physicalRows, cols, reservedRows, restoreRow = 1, restoreCol = 1) {
|
|
2333
|
-
let rows = Number(physicalRows || 24);
|
|
2334
|
-
rows = Number.isFinite(rows) ? rows : 24;
|
|
2335
|
-
cols = Number(cols || 80);
|
|
2336
|
-
cols = Number.isFinite(cols) ? cols : 80;
|
|
2337
|
-
let reserved = Number(reservedRows || 1);
|
|
2338
|
-
reserved = Number.isFinite(reserved) ? Math.max(1, Math.trunc(reserved)) : 1;
|
|
2339
|
-
|
|
2340
|
-
reserved = Math.min(reserved, rows);
|
|
2341
|
-
const startRow = rows - reserved + 1;
|
|
2342
|
-
const parts = ["\\x1b[?2026h", "\\x1b[?25l", RESET_SGR];
|
|
2343
|
-
for (let i = 0; i < reserved; i++) parts.push("\\x1b[" + (startRow + i) + ";1H\\x1b[2K");
|
|
2344
|
-
parts.push("\\x1b[" + restoreRow + ";" + restoreCol + "H");
|
|
2345
|
-
parts.push(this.cursorVisible ? "\\x1b[?25h" : "\\x1b[?25l");
|
|
2346
|
-
parts.push("\\x1b[?2026l");
|
|
2347
|
-
writeStdout(parts.join(""));
|
|
2348
|
-
}
|
|
2349
|
-
render(physicalRows, cols, restoreRow = 1, restoreCol = 1) {
|
|
2350
|
-
if (!this.force) return;
|
|
2351
|
-
if (!this.raw) {
|
|
2352
|
-
this.force = false;
|
|
2353
|
-
this.urgent = false;
|
|
2354
|
-
return;
|
|
2355
|
-
}
|
|
2356
|
-
const now = Date.now();
|
|
2357
|
-
if (!this.urgent && now - this.lastRenderMs < MIN_RENDER_INTERVAL_MS) return;
|
|
2358
|
-
if (!this.urgent && QUIET_MS > 0 && now - this.lastChildOutMs < QUIET_MS) return;
|
|
2359
|
-
|
|
2360
|
-
let rows = Number(physicalRows || 24);
|
|
2361
|
-
rows = Number.isFinite(rows) ? rows : 24;
|
|
2362
|
-
cols = Number(cols || 80);
|
|
2363
|
-
cols = Number.isFinite(cols) ? cols : 80;
|
|
2364
|
-
if (cols <= 0) cols = 80;
|
|
2365
|
-
|
|
2366
|
-
const reserved = Math.max(1, Math.min(this.activeReservedRows, Math.max(1, rows - 4)));
|
|
2367
|
-
const startRow = rows - reserved + 1;
|
|
2368
|
-
const childRows = rows - reserved;
|
|
2369
|
-
|
|
2370
|
-
let lines = this.lines.length ? this.lines.slice() : [""];
|
|
2371
|
-
if (lines.length < reserved) lines = new Array(reserved - lines.length).fill("").concat(lines);
|
|
2372
|
-
if (lines.length > reserved) lines = lines.slice(-reserved);
|
|
2373
|
-
|
|
2374
|
-
const parts = ["\\x1b[?2026h", "\\x1b[?25l"];
|
|
2375
|
-
parts.push("\\x1b[1;" + childRows + "r");
|
|
2376
|
-
for (let i = 0; i < reserved; i++) {
|
|
2377
|
-
const row = startRow + i;
|
|
2378
|
-
const text = clampAnsi(lines[i], cols);
|
|
2379
|
-
parts.push("\\x1b[" + row + ";1H" + RESET_SGR + "\\x1b[2K");
|
|
2380
|
-
parts.push("\\x1b[" + row + ";1H" + text + RESET_SGR);
|
|
2381
|
-
}
|
|
2382
|
-
parts.push("\\x1b[" + restoreRow + ";" + restoreCol + "H");
|
|
2383
|
-
parts.push(this.cursorVisible ? "\\x1b[?25h" : "\\x1b[?25l");
|
|
2384
|
-
parts.push("\\x1b[?2026l");
|
|
2385
|
-
writeStdout(parts.join(""));
|
|
2386
|
-
|
|
2387
|
-
this.lastRenderMs = now;
|
|
2388
|
-
this.force = false;
|
|
2389
|
-
this.urgent = false;
|
|
2390
|
-
}
|
|
2391
|
-
clear() {
|
|
2392
|
-
const { rows, cols } = termSize();
|
|
2393
|
-
this.clearReservedArea(rows, cols, Math.max(this.activeReservedRows, RESERVED_ROWS));
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
|
-
class OutputRewriter {
|
|
2398
|
-
constructor() {
|
|
2399
|
-
this.buf = new Uint8Array(0);
|
|
2400
|
-
}
|
|
2401
|
-
feed(chunk, maxRow) {
|
|
2402
|
-
if (!chunk || chunk.length === 0) return chunk;
|
|
2403
|
-
const merged = new Uint8Array(this.buf.length + chunk.length);
|
|
2404
|
-
merged.set(this.buf, 0);
|
|
2405
|
-
merged.set(chunk, this.buf.length);
|
|
2406
|
-
this.buf = new Uint8Array(0);
|
|
2407
|
-
|
|
2408
|
-
const out = [];
|
|
2409
|
-
let i = 0;
|
|
2410
|
-
|
|
2411
|
-
const isFinal = (v) => v >= 0x40 && v <= 0x7e;
|
|
2412
|
-
|
|
2413
|
-
while (i < merged.length) {
|
|
2414
|
-
const b = merged[i];
|
|
2415
|
-
if (b !== 0x1b) {
|
|
2416
|
-
out.push(b);
|
|
2417
|
-
i += 1;
|
|
2418
|
-
continue;
|
|
2419
|
-
}
|
|
2420
|
-
if (i + 1 >= merged.length) {
|
|
2421
|
-
this.buf = merged.slice(i);
|
|
2422
|
-
break;
|
|
2423
|
-
}
|
|
2424
|
-
const nxt = merged[i + 1];
|
|
2425
|
-
if (nxt !== 0x5b) {
|
|
2426
|
-
out.push(b);
|
|
2427
|
-
i += 1;
|
|
2428
|
-
continue;
|
|
2429
|
-
}
|
|
2430
|
-
|
|
2431
|
-
let j = i + 2;
|
|
2432
|
-
while (j < merged.length && !isFinal(merged[j])) j += 1;
|
|
2433
|
-
if (j >= merged.length) {
|
|
2434
|
-
this.buf = merged.slice(i);
|
|
2435
|
-
break;
|
|
2436
|
-
}
|
|
2437
|
-
const final = merged[j];
|
|
2438
|
-
let seq = merged.slice(i, j + 1);
|
|
2439
|
-
|
|
2440
|
-
if ((final === 0x48 || final === 0x66) && maxRow > 0) {
|
|
2441
|
-
const params = merged.slice(i + 2, j);
|
|
2442
|
-
const s = new TextDecoder().decode(params);
|
|
2443
|
-
if (!s || /^[0-9;]/.test(s)) {
|
|
2444
|
-
const parts = s ? s.split(";") : [];
|
|
2445
|
-
const row = Number(parts[0] || 1);
|
|
2446
|
-
const col = Number(parts[1] || 1);
|
|
2447
|
-
let r = Number.isFinite(row) ? row : 1;
|
|
2448
|
-
let c = Number.isFinite(col) ? col : 1;
|
|
2449
|
-
if (r === 999 || r > maxRow) r = maxRow;
|
|
2450
|
-
if (r < 1) r = 1;
|
|
2451
|
-
if (c < 1) c = 1;
|
|
2452
|
-
const newParams = new TextEncoder().encode(String(r) + ";" + String(c));
|
|
2453
|
-
const ns = new Uint8Array(2 + newParams.length + 1);
|
|
2454
|
-
ns[0] = 0x1b;
|
|
2455
|
-
ns[1] = 0x5b;
|
|
2456
|
-
ns.set(newParams, 2);
|
|
2457
|
-
ns[ns.length - 1] = final;
|
|
2458
|
-
seq = ns;
|
|
2459
|
-
}
|
|
2460
|
-
} else if (final === 0x72 && maxRow > 0) {
|
|
2461
|
-
const params = merged.slice(i + 2, j);
|
|
2462
|
-
const s = new TextDecoder().decode(params);
|
|
2463
|
-
if (!s || /^[0-9;]/.test(s)) {
|
|
2464
|
-
const parts = s ? s.split(";") : [];
|
|
2465
|
-
const top = Number(parts[0] || 1);
|
|
2466
|
-
const bottom = Number(parts[1] || maxRow);
|
|
2467
|
-
let t = Number.isFinite(top) ? top : 1;
|
|
2468
|
-
let btm = Number.isFinite(bottom) ? bottom : maxRow;
|
|
2469
|
-
if (t <= 0) t = 1;
|
|
2470
|
-
if (btm <= 0 || btm === 999 || btm > maxRow) btm = maxRow;
|
|
2471
|
-
if (t > btm) t = 1;
|
|
2472
|
-
const str = "\\x1b[" + String(t) + ";" + String(btm) + "r";
|
|
2473
|
-
seq = new TextEncoder().encode(str);
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
|
-
for (const bb of seq) out.push(bb);
|
|
2478
|
-
i = j + 1;
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
return new Uint8Array(out);
|
|
2482
|
-
}
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
class CursorTracker {
|
|
2486
|
-
constructor() {
|
|
2487
|
-
this.row = 1;
|
|
2488
|
-
this.col = 1;
|
|
2489
|
-
this.savedRow = 1;
|
|
2490
|
-
this.savedCol = 1;
|
|
2491
|
-
this.buf = new Uint8Array(0);
|
|
2492
|
-
this.inOsc = false;
|
|
2493
|
-
this.utf8Cont = 0;
|
|
2494
|
-
this.wrapPending = false;
|
|
2495
|
-
}
|
|
2496
|
-
position() {
|
|
2497
|
-
return { row: this.row, col: this.col };
|
|
2498
|
-
}
|
|
2499
|
-
feed(chunk, maxRow, maxCol) {
|
|
2500
|
-
if (!chunk || chunk.length === 0) return;
|
|
2501
|
-
maxRow = Math.max(1, Number(maxRow || 1));
|
|
2502
|
-
maxCol = Math.max(1, Number(maxCol || 1));
|
|
2503
|
-
|
|
2504
|
-
const merged = new Uint8Array(this.buf.length + chunk.length);
|
|
2505
|
-
merged.set(this.buf, 0);
|
|
2506
|
-
merged.set(chunk, this.buf.length);
|
|
2507
|
-
this.buf = new Uint8Array(0);
|
|
2508
|
-
|
|
2509
|
-
const clamp = () => {
|
|
2510
|
-
if (this.row < 1) this.row = 1;
|
|
2511
|
-
else if (this.row > maxRow) this.row = maxRow;
|
|
2512
|
-
if (this.col < 1) this.col = 1;
|
|
2513
|
-
else if (this.col > maxCol) this.col = maxCol;
|
|
2514
|
-
};
|
|
2515
|
-
|
|
2516
|
-
const parseIntDefault = (v, d) => {
|
|
2517
|
-
const n = Number(v);
|
|
2518
|
-
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : d;
|
|
2519
|
-
};
|
|
2520
|
-
|
|
2521
|
-
let i = 0;
|
|
2522
|
-
const isFinal = (v) => v >= 0x40 && v <= 0x7e;
|
|
2523
|
-
|
|
2524
|
-
while (i < merged.length) {
|
|
2525
|
-
const b = merged[i];
|
|
2526
|
-
|
|
2527
|
-
if (this.inOsc) {
|
|
2528
|
-
if (b === 0x07) {
|
|
2529
|
-
this.inOsc = false;
|
|
2530
|
-
i += 1;
|
|
2531
|
-
continue;
|
|
2532
|
-
}
|
|
2533
|
-
if (b === 0x1b) {
|
|
2534
|
-
if (i + 1 >= merged.length) {
|
|
2535
|
-
this.buf = merged.slice(i);
|
|
2536
|
-
break;
|
|
2537
|
-
}
|
|
2538
|
-
if (merged[i + 1] === 0x5c) {
|
|
2539
|
-
this.inOsc = false;
|
|
2540
|
-
i += 2;
|
|
2541
|
-
continue;
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
i += 1;
|
|
2545
|
-
continue;
|
|
2546
|
-
}
|
|
2547
|
-
|
|
2548
|
-
if (this.utf8Cont > 0) {
|
|
2549
|
-
if (b >= 0x80 && b <= 0xbf) {
|
|
2550
|
-
this.utf8Cont -= 1;
|
|
2551
|
-
i += 1;
|
|
2552
|
-
continue;
|
|
2553
|
-
}
|
|
2554
|
-
this.utf8Cont = 0;
|
|
2555
|
-
}
|
|
2556
|
-
|
|
2557
|
-
if (b === 0x1b) {
|
|
2558
|
-
this.wrapPending = false;
|
|
2559
|
-
if (i + 1 >= merged.length) {
|
|
2560
|
-
this.buf = merged.slice(i);
|
|
2561
|
-
break;
|
|
2562
|
-
}
|
|
2563
|
-
const nxt = merged[i + 1];
|
|
2564
|
-
|
|
2565
|
-
if (nxt === 0x5b) {
|
|
2566
|
-
let j = i + 2;
|
|
2567
|
-
while (j < merged.length && !isFinal(merged[j])) j += 1;
|
|
2568
|
-
if (j >= merged.length) {
|
|
2569
|
-
this.buf = merged.slice(i);
|
|
2570
|
-
break;
|
|
2571
|
-
}
|
|
2572
|
-
const final = merged[j];
|
|
2573
|
-
const params = merged.slice(i + 2, j);
|
|
2574
|
-
const s = new TextDecoder().decode(params);
|
|
2575
|
-
if (s && !/^[0-9;]/.test(s)) {
|
|
2576
|
-
i = j + 1;
|
|
2577
|
-
continue;
|
|
2578
|
-
}
|
|
2579
|
-
const parts = s ? s.split(";") : [];
|
|
2580
|
-
const p0 = parseIntDefault(parts[0] || "", 1);
|
|
2581
|
-
const p1 = parseIntDefault(parts[1] || "", 1);
|
|
2582
|
-
|
|
2583
|
-
if (final === 0x48 || final === 0x66) {
|
|
2584
|
-
this.row = p0;
|
|
2585
|
-
this.col = p1;
|
|
2586
|
-
clamp();
|
|
2587
|
-
} else if (final === 0x41) {
|
|
2588
|
-
this.row = Math.max(1, this.row - p0);
|
|
2589
|
-
} else if (final === 0x42) {
|
|
2590
|
-
this.row = Math.min(maxRow, this.row + p0);
|
|
2591
|
-
} else if (final === 0x43) {
|
|
2592
|
-
this.col = Math.min(maxCol, this.col + p0);
|
|
2593
|
-
} else if (final === 0x44) {
|
|
2594
|
-
this.col = Math.max(1, this.col - p0);
|
|
2595
|
-
} else if (final === 0x45) {
|
|
2596
|
-
this.row = Math.min(maxRow, this.row + p0);
|
|
2597
|
-
this.col = 1;
|
|
2598
|
-
} else if (final === 0x46) {
|
|
2599
|
-
this.row = Math.max(1, this.row - p0);
|
|
2600
|
-
this.col = 1;
|
|
2601
|
-
} else if (final === 0x47) {
|
|
2602
|
-
this.col = p0;
|
|
2603
|
-
clamp();
|
|
2604
|
-
} else if (final === 0x64) {
|
|
2605
|
-
this.row = p0;
|
|
2606
|
-
clamp();
|
|
2607
|
-
} else if (final === 0x72) {
|
|
2608
|
-
this.row = 1;
|
|
2609
|
-
this.col = 1;
|
|
2610
|
-
} else if (final === 0x73) {
|
|
2611
|
-
this.savedRow = this.row;
|
|
2612
|
-
this.savedCol = this.col;
|
|
2613
|
-
} else if (final === 0x75) {
|
|
2614
|
-
this.row = this.savedRow;
|
|
2615
|
-
this.col = this.savedCol;
|
|
2616
|
-
clamp();
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
i = j + 1;
|
|
2620
|
-
continue;
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
if (nxt === 0x5d || nxt === 0x50 || nxt === 0x5e || nxt === 0x5f || nxt === 0x58) {
|
|
2624
|
-
this.inOsc = true;
|
|
2625
|
-
i += 2;
|
|
2626
|
-
continue;
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
if (nxt === 0x37) {
|
|
2630
|
-
this.savedRow = this.row;
|
|
2631
|
-
this.savedCol = this.col;
|
|
2632
|
-
i += 2;
|
|
2633
|
-
continue;
|
|
2634
|
-
}
|
|
2635
|
-
if (nxt === 0x38) {
|
|
2636
|
-
this.row = this.savedRow;
|
|
2637
|
-
this.col = this.savedCol;
|
|
2638
|
-
clamp();
|
|
2639
|
-
i += 2;
|
|
2640
|
-
continue;
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
i += 2;
|
|
2644
|
-
continue;
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
|
-
if (b === 0x0d) {
|
|
2648
|
-
this.col = 1;
|
|
2649
|
-
this.wrapPending = false;
|
|
2650
|
-
i += 1;
|
|
2651
|
-
continue;
|
|
2652
|
-
}
|
|
2653
|
-
if (b === 0x0a || b === 0x0b || b === 0x0c) {
|
|
2654
|
-
this.row = Math.min(maxRow, this.row + 1);
|
|
2655
|
-
this.wrapPending = false;
|
|
2656
|
-
i += 1;
|
|
2657
|
-
continue;
|
|
2658
|
-
}
|
|
2659
|
-
if (b === 0x08) {
|
|
2660
|
-
this.col = Math.max(1, this.col - 1);
|
|
2661
|
-
this.wrapPending = false;
|
|
2662
|
-
i += 1;
|
|
2663
|
-
continue;
|
|
2664
|
-
}
|
|
2665
|
-
if (b === 0x09) {
|
|
2666
|
-
const nextStop = Math.floor((this.col - 1) / 8 + 1) * 8 + 1;
|
|
2667
|
-
this.col = Math.min(maxCol, nextStop);
|
|
2668
|
-
this.wrapPending = false;
|
|
2669
|
-
i += 1;
|
|
2670
|
-
continue;
|
|
2671
|
-
}
|
|
2672
|
-
if (b < 0x20 || b === 0x7f) {
|
|
2673
|
-
i += 1;
|
|
2674
|
-
continue;
|
|
2675
|
-
}
|
|
2676
|
-
|
|
2677
|
-
if (this.wrapPending) {
|
|
2678
|
-
this.row = Math.min(maxRow, this.row + 1);
|
|
2679
|
-
this.col = 1;
|
|
2680
|
-
this.wrapPending = false;
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
if (b >= 0x80) {
|
|
2684
|
-
if ((b & 0xe0) === 0xc0) this.utf8Cont = 1;
|
|
2685
|
-
else if ((b & 0xf0) === 0xe0) this.utf8Cont = 2;
|
|
2686
|
-
else if ((b & 0xf8) === 0xf0) this.utf8Cont = 3;
|
|
2687
|
-
else this.utf8Cont = 0;
|
|
2688
|
-
}
|
|
2689
|
-
|
|
2690
|
-
if (this.col < maxCol) this.col += 1;
|
|
2691
|
-
else {
|
|
2692
|
-
this.col = maxCol;
|
|
2693
|
-
this.wrapPending = true;
|
|
2694
|
-
}
|
|
2695
|
-
i += 1;
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
async function main() {
|
|
2701
|
-
const argv = process.argv.slice(2);
|
|
2702
|
-
|
|
2703
|
-
if (isSessionsCommand(argv)) await runSessions();
|
|
2704
|
-
|
|
2705
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY || shouldPassthrough(argv)) {
|
|
2706
|
-
await execPassthrough(argv);
|
|
2707
|
-
return;
|
|
2708
|
-
}
|
|
2709
|
-
|
|
2710
|
-
// Clean viewport.
|
|
2711
|
-
writeStdout("\\x1b[?2026h\\x1b[0m\\x1b[r\\x1b[2J\\x1b[H\\x1b[?2026l");
|
|
2712
|
-
|
|
2713
|
-
const renderer = new StatusRenderer();
|
|
2714
|
-
renderer.setLine("\\x1b[48;5;238m\\x1b[38;5;15m Statusline: starting… \\x1b[0m");
|
|
2715
|
-
renderer.forceRepaint(true);
|
|
2716
|
-
|
|
2717
|
-
let { rows: physicalRows, cols: physicalCols } = termSize();
|
|
2718
|
-
let effectiveReservedRows = renderer.desiredReservedRows(physicalRows, physicalCols, RESERVED_ROWS);
|
|
2719
|
-
renderer.setActiveReservedRows(effectiveReservedRows);
|
|
2720
|
-
let childRows = Math.max(4, physicalRows - effectiveReservedRows);
|
|
2721
|
-
let childCols = Math.max(10, physicalCols);
|
|
2722
|
-
|
|
2723
|
-
// Reserve the bottom rows early, before the child starts writing.
|
|
2724
|
-
writeStdout(
|
|
2725
|
-
"\\x1b[?2026h\\x1b[?25l\\x1b[1;" + childRows + "r\\x1b[1;1H\\x1b[?25h\\x1b[?2026l",
|
|
2726
|
-
);
|
|
2727
|
-
renderer.forceRepaint(true);
|
|
2728
|
-
renderer.render(physicalRows, physicalCols, 1, 1);
|
|
2729
|
-
|
|
2730
|
-
// Spawn child with terminal support.
|
|
2731
|
-
let child;
|
|
2732
|
-
try {
|
|
2733
|
-
child = Bun.spawn([EXEC_TARGET, ...argv], {
|
|
2734
|
-
cwd: process.cwd(),
|
|
2735
|
-
env: process.env,
|
|
2736
|
-
detached: true,
|
|
2737
|
-
terminal: {
|
|
2738
|
-
cols: childCols,
|
|
2739
|
-
rows: childRows,
|
|
2740
|
-
data(_terminal, data) {
|
|
2741
|
-
onChildData(data);
|
|
2742
|
-
},
|
|
2743
|
-
},
|
|
2744
|
-
onExit(_proc, exitCode, signal, _error) {
|
|
2745
|
-
onChildExit(exitCode, signal);
|
|
2746
|
-
},
|
|
2747
|
-
});
|
|
2748
|
-
} catch (e) {
|
|
2749
|
-
process.stderr.write("[statusline] failed to spawn child: " + String(e?.message || e) + "\\n");
|
|
2750
|
-
process.exit(1);
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
|
-
const terminal = child.terminal;
|
|
2754
|
-
|
|
2755
|
-
// Best-effort PGID resolution (matches Python wrapper behavior).
|
|
2756
|
-
// This improves session resolution (ps/lsof scanning) and signal forwarding.
|
|
2757
|
-
let pgid = child.pid;
|
|
2758
|
-
try {
|
|
2759
|
-
const res = Bun.spawnSync(["ps", "-o", "pgid=", "-p", String(child.pid)], {
|
|
2760
|
-
stdin: "ignore",
|
|
2761
|
-
stdout: "pipe",
|
|
2762
|
-
stderr: "ignore",
|
|
2763
|
-
});
|
|
2764
|
-
if (res && res.exitCode === 0 && res.stdout) {
|
|
2765
|
-
const text = new TextDecoder().decode(res.stdout).trim();
|
|
2766
|
-
const n = Number(text);
|
|
2767
|
-
if (Number.isFinite(n) && n > 0) pgid = Math.trunc(n);
|
|
2768
|
-
}
|
|
2769
|
-
} catch {}
|
|
2770
|
-
|
|
2771
|
-
// Spawn monitor (Node).
|
|
2772
|
-
const monitorEnv = { ...process.env, DROID_STATUSLINE_PGID: String(pgid) };
|
|
2773
|
-
const monitor = Bun.spawn(["node", STATUSLINE_MONITOR, ...argv], {
|
|
2774
|
-
stdin: "ignore",
|
|
2775
|
-
stdout: "pipe",
|
|
2776
|
-
stderr: "ignore",
|
|
2777
|
-
env: monitorEnv,
|
|
2778
|
-
});
|
|
2779
|
-
|
|
2780
|
-
let shouldStop = false;
|
|
2781
|
-
const rewriter = new OutputRewriter();
|
|
2782
|
-
const cursor = new CursorTracker();
|
|
2783
|
-
|
|
2784
|
-
let detectBuf = new Uint8Array(0);
|
|
2785
|
-
let detectStr = "";
|
|
2786
|
-
let cursorVisible = true;
|
|
2787
|
-
let scrollRegionDirty = true;
|
|
2788
|
-
let lastForceRepaintMs = Date.now();
|
|
2789
|
-
let lastPhysicalRows = 0;
|
|
2790
|
-
let lastPhysicalCols = 0;
|
|
2791
|
-
|
|
2792
|
-
function appendDetect(chunk) {
|
|
2793
|
-
const max = 128;
|
|
2794
|
-
const merged = new Uint8Array(Math.min(max, detectBuf.length + chunk.length));
|
|
2795
|
-
const takePrev = Math.max(0, merged.length - chunk.length);
|
|
2796
|
-
if (takePrev > 0) merged.set(detectBuf.slice(Math.max(0, detectBuf.length - takePrev)), 0);
|
|
2797
|
-
merged.set(chunk.slice(Math.max(0, chunk.length - (merged.length - takePrev))), takePrev);
|
|
2798
|
-
detectBuf = merged;
|
|
2799
|
-
try {
|
|
2800
|
-
detectStr = Buffer.from(detectBuf).toString("latin1");
|
|
2801
|
-
} catch {
|
|
2802
|
-
detectStr = "";
|
|
2803
|
-
}
|
|
2804
|
-
}
|
|
2805
|
-
|
|
2806
|
-
function includesBytes(needle) {
|
|
2807
|
-
return detectStr.includes(needle);
|
|
2808
|
-
}
|
|
2809
|
-
|
|
2810
|
-
function lastIndexOfBytes(needle) {
|
|
2811
|
-
return detectStr.lastIndexOf(needle);
|
|
2812
|
-
}
|
|
2813
|
-
|
|
2814
|
-
function includesScrollRegionCSI() {
|
|
2815
|
-
return /\\x1b\\[[0-9]*;?[0-9]*r/.test(detectStr);
|
|
2816
|
-
}
|
|
2817
|
-
|
|
2818
|
-
function updateCursorVisibility() {
|
|
2819
|
-
const show = includesBytes("\\x1b[?25h");
|
|
2820
|
-
const hide = includesBytes("\\x1b[?25l");
|
|
2821
|
-
if (show || hide) {
|
|
2822
|
-
// best-effort: if both present, whichever appears later "wins"
|
|
2823
|
-
const h = lastIndexOfBytes("\\x1b[?25h");
|
|
2824
|
-
const l = lastIndexOfBytes("\\x1b[?25l");
|
|
2825
|
-
cursorVisible = h > l;
|
|
2826
|
-
renderer.setCursorVisible(cursorVisible);
|
|
2827
|
-
}
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
function needsScrollRegionReset() {
|
|
2831
|
-
return (
|
|
2832
|
-
includesBytes("\\x1b[?1049") ||
|
|
2833
|
-
includesBytes("\\x1b[?1047") ||
|
|
2834
|
-
includesBytes("\\x1b[?47") ||
|
|
2835
|
-
includesBytes("\\x1b[J") ||
|
|
2836
|
-
includesBytes("\\x1b[0J") ||
|
|
2837
|
-
includesBytes("\\x1b[1J") ||
|
|
2838
|
-
includesBytes("\\x1b[2J") ||
|
|
2839
|
-
includesBytes("\\x1b[3J") ||
|
|
2840
|
-
includesBytes("\\x1b[r") ||
|
|
2841
|
-
includesScrollRegionCSI()
|
|
2842
|
-
);
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
function onChildData(data) {
|
|
2846
|
-
if (shouldStop) return;
|
|
2847
|
-
const chunk = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
2848
|
-
appendDetect(chunk);
|
|
2849
|
-
if (needsScrollRegionReset()) scrollRegionDirty = true;
|
|
2850
|
-
updateCursorVisibility();
|
|
2851
|
-
|
|
2852
|
-
renderer.noteChildOutput();
|
|
2853
|
-
const rewritten = rewriter.feed(chunk, childRows);
|
|
2854
|
-
cursor.feed(rewritten, childRows, childCols);
|
|
2855
|
-
writeStdout(Buffer.from(rewritten));
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
let cleanupCalled = false;
|
|
2859
|
-
function onChildExit(exitCode, signal) {
|
|
2860
|
-
shouldStop = true;
|
|
2861
|
-
if (cleanupCalled) return;
|
|
2862
|
-
cleanupCalled = true;
|
|
2863
|
-
const code = exitCode ?? (signal != null ? 128 + signal : 0);
|
|
2864
|
-
cleanup().finally(() => process.exit(code));
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
async function readMonitor() {
|
|
2868
|
-
if (!monitor.stdout) return;
|
|
2869
|
-
const reader = monitor.stdout.getReader();
|
|
2870
|
-
let buf = "";
|
|
2871
|
-
while (!shouldStop) {
|
|
2872
|
-
const { value, done } = await reader.read();
|
|
2873
|
-
if (done || !value) break;
|
|
2874
|
-
buf += new TextDecoder().decode(value);
|
|
2875
|
-
while (true) {
|
|
2876
|
-
const idx = buf.indexOf("\\n");
|
|
2877
|
-
if (idx === -1) break;
|
|
2878
|
-
const line = buf.slice(0, idx).replace(/\\r$/, "");
|
|
2879
|
-
buf = buf.slice(idx + 1);
|
|
2880
|
-
if (!line) continue;
|
|
2881
|
-
renderer.setLine(line);
|
|
2882
|
-
renderer.forceRepaint(false);
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
}
|
|
2886
|
-
readMonitor().catch(() => {});
|
|
2887
|
-
|
|
2888
|
-
function repaintStatusline(forceUrgent = false) {
|
|
2889
|
-
const { row, col } = cursor.position();
|
|
2890
|
-
let r = Math.max(1, Math.min(childRows, row));
|
|
2891
|
-
let c = Math.max(1, Math.min(childCols, col));
|
|
2892
|
-
|
|
2893
|
-
if (scrollRegionDirty) {
|
|
2894
|
-
const seq =
|
|
2895
|
-
"\\x1b[?2026h\\x1b[?25l\\x1b[1;" +
|
|
2896
|
-
childRows +
|
|
2897
|
-
"r\\x1b[" +
|
|
2898
|
-
r +
|
|
2899
|
-
";" +
|
|
2900
|
-
c +
|
|
2901
|
-
"H" +
|
|
2902
|
-
(cursorVisible ? "\\x1b[?25h" : "\\x1b[?25l") +
|
|
2903
|
-
"\\x1b[?2026l";
|
|
2904
|
-
writeStdout(seq);
|
|
2905
|
-
scrollRegionDirty = false;
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
|
-
renderer.forceRepaint(forceUrgent);
|
|
2909
|
-
renderer.render(physicalRows, physicalCols, r, c);
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
function handleSizeChange(nextRows, nextCols, forceUrgent = false) {
|
|
2913
|
-
physicalRows = nextRows;
|
|
2914
|
-
physicalCols = nextCols;
|
|
2915
|
-
|
|
2916
|
-
const desired = renderer.desiredReservedRows(physicalRows, physicalCols, RESERVED_ROWS);
|
|
2917
|
-
const { row, col } = cursor.position();
|
|
2918
|
-
if (desired < effectiveReservedRows) {
|
|
2919
|
-
renderer.clearReservedArea(physicalRows, physicalCols, effectiveReservedRows, row, col);
|
|
2920
|
-
}
|
|
2921
|
-
effectiveReservedRows = desired;
|
|
2922
|
-
renderer.setActiveReservedRows(effectiveReservedRows);
|
|
2923
|
-
|
|
2924
|
-
childRows = Math.max(4, physicalRows - effectiveReservedRows);
|
|
2925
|
-
childCols = Math.max(10, physicalCols);
|
|
2926
|
-
try {
|
|
2927
|
-
terminal.resize(childCols, childRows);
|
|
2928
|
-
} catch {}
|
|
2929
|
-
try {
|
|
2930
|
-
process.kill(-child.pid, "SIGWINCH");
|
|
2931
|
-
} catch {
|
|
2932
|
-
try { process.kill(child.pid, "SIGWINCH"); } catch {}
|
|
2933
|
-
}
|
|
2934
|
-
|
|
2935
|
-
scrollRegionDirty = true;
|
|
2936
|
-
renderer.forceRepaint(true);
|
|
2937
|
-
repaintStatusline(forceUrgent);
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
|
-
process.on("SIGWINCH", () => {
|
|
2941
|
-
const next = termSize();
|
|
2942
|
-
handleSizeChange(next.rows, next.cols, true);
|
|
2943
|
-
});
|
|
2944
|
-
|
|
2945
|
-
// Forward signals to child's process group when possible.
|
|
2946
|
-
const forward = (sig) => {
|
|
2947
|
-
// Stop processing child output before forwarding signal
|
|
2948
|
-
// This prevents the child's cleanup/clear screen sequences from being written
|
|
2949
|
-
shouldStop = true;
|
|
2950
|
-
try {
|
|
2951
|
-
process.kill(-pgid, sig);
|
|
2952
|
-
} catch {
|
|
2953
|
-
try {
|
|
2954
|
-
process.kill(child.pid, sig);
|
|
2955
|
-
} catch {}
|
|
2956
|
-
}
|
|
2957
|
-
};
|
|
2958
|
-
for (const s of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
2959
|
-
try {
|
|
2960
|
-
process.on(s, () => forward(s));
|
|
2961
|
-
} catch {}
|
|
2962
|
-
}
|
|
2963
|
-
|
|
2964
|
-
// Raw stdin -> PTY.
|
|
2965
|
-
try {
|
|
2966
|
-
process.stdin.setRawMode(true);
|
|
2967
|
-
} catch {}
|
|
2968
|
-
process.stdin.resume();
|
|
2969
|
-
process.stdin.on("data", (buf) => {
|
|
2970
|
-
try {
|
|
2971
|
-
if (typeof buf === "string") terminal.write(buf);
|
|
2972
|
-
else {
|
|
2973
|
-
// Prefer bytes when supported; fall back to UTF-8 decoding.
|
|
2974
|
-
try {
|
|
2975
|
-
// Bun.Terminal.write may accept Uint8Array in newer versions.
|
|
2976
|
-
terminal.write(buf);
|
|
2977
|
-
} catch {
|
|
2978
|
-
terminal.write(new TextDecoder().decode(buf));
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
|
-
} catch {}
|
|
2982
|
-
});
|
|
2983
|
-
|
|
2984
|
-
const tick = setInterval(() => {
|
|
2985
|
-
if (shouldStop) return;
|
|
2986
|
-
const next = termSize();
|
|
2987
|
-
const sizeChanged = next.rows !== lastPhysicalRows || next.cols !== lastPhysicalCols;
|
|
2988
|
-
const desired = renderer.desiredReservedRows(next.rows, next.cols, RESERVED_ROWS);
|
|
2989
|
-
if (sizeChanged || desired !== effectiveReservedRows) {
|
|
2990
|
-
handleSizeChange(next.rows, next.cols, true);
|
|
2991
|
-
lastPhysicalRows = next.rows;
|
|
2992
|
-
lastPhysicalCols = next.cols;
|
|
2993
|
-
lastForceRepaintMs = Date.now();
|
|
2994
|
-
return;
|
|
2995
|
-
}
|
|
2996
|
-
const now = Date.now();
|
|
2997
|
-
if (now - lastForceRepaintMs >= FORCE_REPAINT_INTERVAL_MS) {
|
|
2998
|
-
repaintStatusline(false);
|
|
2999
|
-
lastForceRepaintMs = now;
|
|
3000
|
-
} else {
|
|
3001
|
-
const { row, col } = cursor.position();
|
|
3002
|
-
renderer.render(physicalRows, physicalCols, row, col);
|
|
3003
|
-
}
|
|
3004
|
-
}, 50);
|
|
3005
|
-
|
|
3006
|
-
async function cleanup() {
|
|
3007
|
-
clearInterval(tick);
|
|
3008
|
-
try {
|
|
3009
|
-
process.stdin.setRawMode(false);
|
|
3010
|
-
} catch {}
|
|
3011
|
-
// Don't clear screen or reset scroll region on exit - preserve session ID and logs
|
|
3012
|
-
// Only reset colors and show cursor
|
|
3013
|
-
try {
|
|
3014
|
-
writeStdout("\\x1b[0m\\x1b[?25h");
|
|
3015
|
-
} catch {}
|
|
3016
|
-
try {
|
|
3017
|
-
monitor.kill();
|
|
3018
|
-
} catch {}
|
|
3019
|
-
try {
|
|
3020
|
-
terminal.close();
|
|
3021
|
-
} catch {}
|
|
3022
|
-
}
|
|
3023
|
-
|
|
3024
|
-
// Keep process alive until child exits.
|
|
3025
|
-
await child.exited;
|
|
3026
|
-
await cleanup();
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
main().catch(() => process.exit(1));
|
|
3030
|
-
`;
|
|
3031
|
-
}
|
|
3032
|
-
async function createStatuslineFiles(outputDir, execTargetPath, aliasName, sessionsScriptPath) {
|
|
3033
|
-
if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
|
|
3034
|
-
const monitorScriptPath = join(outputDir, `${aliasName}-statusline.js`);
|
|
3035
|
-
const wrapperScriptPath = join(outputDir, aliasName);
|
|
3036
|
-
await writeFile(monitorScriptPath, generateStatuslineMonitorScript());
|
|
3037
|
-
await chmod(monitorScriptPath, 493);
|
|
3038
|
-
await writeFile(wrapperScriptPath, generateStatuslineWrapperScript(execTargetPath, monitorScriptPath, sessionsScriptPath));
|
|
3039
|
-
await chmod(wrapperScriptPath, 493);
|
|
3040
|
-
return {
|
|
3041
|
-
wrapperScript: wrapperScriptPath,
|
|
3042
|
-
monitorScript: monitorScriptPath
|
|
3043
|
-
};
|
|
3044
|
-
}
|
|
3045
|
-
|
|
3046
|
-
//#endregion
|
|
3047
|
-
//#region src/sessions-patch.ts
|
|
3048
|
-
/**
|
|
3049
|
-
* Generate sessions browser script (Node.js)
|
|
3050
|
-
*/
|
|
3051
|
-
function generateSessionsBrowserScript(aliasName) {
|
|
3052
|
-
return `#!/usr/bin/env node
|
|
3053
|
-
// Droid Sessions Browser - Interactive selector
|
|
3054
|
-
// Auto-generated by droid-patch
|
|
3055
|
-
|
|
3056
|
-
const fs = require('fs');
|
|
3057
|
-
const path = require('path');
|
|
3058
|
-
const readline = require('readline');
|
|
3059
|
-
const { execSync, spawn } = require('child_process');
|
|
3060
|
-
|
|
3061
|
-
const FACTORY_HOME = path.join(require('os').homedir(), '.factory');
|
|
3062
|
-
const SESSIONS_ROOT = path.join(FACTORY_HOME, 'sessions');
|
|
3063
|
-
const ALIAS_NAME = ${JSON.stringify(aliasName)};
|
|
3064
|
-
|
|
3065
|
-
// ANSI
|
|
3066
|
-
const CYAN = '\\x1b[36m';
|
|
3067
|
-
const GREEN = '\\x1b[32m';
|
|
3068
|
-
const YELLOW = '\\x1b[33m';
|
|
3069
|
-
const RED = '\\x1b[31m';
|
|
3070
|
-
const DIM = '\\x1b[2m';
|
|
3071
|
-
const RESET = '\\x1b[0m';
|
|
3072
|
-
const BOLD = '\\x1b[1m';
|
|
3073
|
-
const CLEAR = '\\x1b[2J\\x1b[H';
|
|
3074
|
-
const HIDE_CURSOR = '\\x1b[?25l';
|
|
3075
|
-
const SHOW_CURSOR = '\\x1b[?25h';
|
|
3076
|
-
|
|
3077
|
-
function sanitizePath(p) {
|
|
3078
|
-
return p.replace(/:/g, '').replace(/[\\\\/]/g, '-');
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
|
-
function parseSessionFile(jsonlPath, settingsPath) {
|
|
3082
|
-
const sessionId = path.basename(jsonlPath, '.jsonl');
|
|
3083
|
-
const stats = fs.statSync(jsonlPath);
|
|
3084
|
-
|
|
3085
|
-
const result = {
|
|
3086
|
-
id: sessionId,
|
|
3087
|
-
title: '',
|
|
3088
|
-
mtime: stats.mtimeMs,
|
|
3089
|
-
model: '',
|
|
3090
|
-
firstUserMsg: '',
|
|
3091
|
-
lastUserMsg: '',
|
|
3092
|
-
messageCount: 0,
|
|
3093
|
-
lastTimestamp: '',
|
|
3094
|
-
};
|
|
3095
|
-
|
|
3096
|
-
try {
|
|
3097
|
-
const content = fs.readFileSync(jsonlPath, 'utf-8');
|
|
3098
|
-
const lines = content.split('\\n').filter(l => l.trim());
|
|
3099
|
-
const userMessages = [];
|
|
3100
|
-
|
|
3101
|
-
for (const line of lines) {
|
|
3102
|
-
try {
|
|
3103
|
-
const obj = JSON.parse(line);
|
|
3104
|
-
if (obj.type === 'session_start') {
|
|
3105
|
-
result.title = obj.title || '';
|
|
3106
|
-
} else if (obj.type === 'message') {
|
|
3107
|
-
result.messageCount++;
|
|
3108
|
-
if (obj.timestamp) result.lastTimestamp = obj.timestamp;
|
|
3109
|
-
|
|
3110
|
-
const msg = obj.message || {};
|
|
3111
|
-
if (msg.role === 'user' && Array.isArray(msg.content)) {
|
|
3112
|
-
for (const c of msg.content) {
|
|
3113
|
-
if (c && c.type === 'text' && c.text && !c.text.startsWith('<system-reminder>')) {
|
|
3114
|
-
userMessages.push(c.text.slice(0, 150).replace(/\\n/g, ' ').trim());
|
|
3115
|
-
break;
|
|
3116
|
-
}
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3120
|
-
} catch {}
|
|
3121
|
-
}
|
|
3122
|
-
|
|
3123
|
-
if (userMessages.length > 0) {
|
|
3124
|
-
result.firstUserMsg = userMessages[0];
|
|
3125
|
-
result.lastUserMsg = userMessages.length > 1 ? userMessages[userMessages.length - 1] : '';
|
|
3126
|
-
}
|
|
3127
|
-
} catch {}
|
|
3128
|
-
|
|
3129
|
-
if (fs.existsSync(settingsPath)) {
|
|
3130
|
-
try {
|
|
3131
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
3132
|
-
result.model = settings.model || '';
|
|
3133
|
-
} catch {}
|
|
3134
|
-
}
|
|
3135
|
-
|
|
3136
|
-
return result;
|
|
3137
|
-
}
|
|
3138
|
-
|
|
3139
|
-
function collectSessions() {
|
|
3140
|
-
const cwd = process.cwd();
|
|
3141
|
-
const cwdSanitized = sanitizePath(cwd);
|
|
3142
|
-
const sessions = [];
|
|
3143
|
-
|
|
3144
|
-
if (!fs.existsSync(SESSIONS_ROOT)) return sessions;
|
|
3145
|
-
|
|
3146
|
-
for (const wsDir of fs.readdirSync(SESSIONS_ROOT)) {
|
|
3147
|
-
if (wsDir !== cwdSanitized) continue;
|
|
3148
|
-
|
|
3149
|
-
const wsPath = path.join(SESSIONS_ROOT, wsDir);
|
|
3150
|
-
if (!fs.statSync(wsPath).isDirectory()) continue;
|
|
3151
|
-
|
|
3152
|
-
for (const file of fs.readdirSync(wsPath)) {
|
|
3153
|
-
if (!file.endsWith('.jsonl')) continue;
|
|
3154
|
-
|
|
3155
|
-
const sessionId = file.slice(0, -6);
|
|
3156
|
-
const jsonlPath = path.join(wsPath, file);
|
|
3157
|
-
const settingsPath = path.join(wsPath, sessionId + '.settings.json');
|
|
3158
|
-
|
|
3159
|
-
try {
|
|
3160
|
-
const session = parseSessionFile(jsonlPath, settingsPath);
|
|
3161
|
-
if (session.messageCount === 0 || !session.firstUserMsg) continue;
|
|
3162
|
-
sessions.push(session);
|
|
3163
|
-
} catch {}
|
|
3164
|
-
}
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
|
-
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
3168
|
-
return sessions.slice(0, 50);
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
|
-
function formatTime(ts) {
|
|
3172
|
-
if (!ts) return '';
|
|
3173
|
-
try {
|
|
3174
|
-
const d = new Date(ts);
|
|
3175
|
-
return d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
3176
|
-
} catch {
|
|
3177
|
-
return ts.slice(0, 16);
|
|
3178
|
-
}
|
|
3179
|
-
}
|
|
3180
|
-
|
|
3181
|
-
function truncate(s, len) {
|
|
3182
|
-
if (!s) return '';
|
|
3183
|
-
s = s.replace(/\\n/g, ' ');
|
|
3184
|
-
return s.length > len ? s.slice(0, len - 3) + '...' : s;
|
|
3185
|
-
}
|
|
3186
|
-
|
|
3187
|
-
function render(sessions, selected, offset, rows) {
|
|
3188
|
-
const cwd = process.cwd();
|
|
3189
|
-
const pageSize = rows - 6;
|
|
3190
|
-
const visible = sessions.slice(offset, offset + pageSize);
|
|
3191
|
-
|
|
3192
|
-
let out = CLEAR;
|
|
3193
|
-
out += BOLD + 'Sessions: ' + RESET + DIM + cwd + RESET + '\\n';
|
|
3194
|
-
out += DIM + '[↑/↓] Select [Enter] Resume [q] Quit' + RESET + '\\n\\n';
|
|
3195
|
-
|
|
3196
|
-
for (let i = 0; i < visible.length; i++) {
|
|
3197
|
-
const s = visible[i];
|
|
3198
|
-
const idx = offset + i;
|
|
3199
|
-
const isSelected = idx === selected;
|
|
3200
|
-
const prefix = isSelected ? GREEN + '▶ ' + RESET : ' ';
|
|
3201
|
-
|
|
3202
|
-
const title = truncate(s.title || '(no title)', 35);
|
|
3203
|
-
const time = formatTime(s.lastTimestamp);
|
|
3204
|
-
const model = truncate(s.model, 20);
|
|
3205
|
-
|
|
3206
|
-
if (isSelected) {
|
|
3207
|
-
out += prefix + YELLOW + title + RESET + '\\n';
|
|
3208
|
-
out += ' ' + DIM + 'ID: ' + RESET + CYAN + s.id + RESET + '\\n';
|
|
3209
|
-
out += ' ' + DIM + 'Last: ' + time + ' | Model: ' + model + ' | ' + s.messageCount + ' msgs' + RESET + '\\n';
|
|
3210
|
-
out += ' ' + DIM + 'First input: ' + RESET + truncate(s.firstUserMsg, 60) + '\\n';
|
|
3211
|
-
if (s.lastUserMsg && s.lastUserMsg !== s.firstUserMsg) {
|
|
3212
|
-
out += ' ' + DIM + 'Last input: ' + RESET + truncate(s.lastUserMsg, 60) + '\\n';
|
|
3213
|
-
}
|
|
3214
|
-
} else {
|
|
3215
|
-
out += prefix + title + DIM + ' (' + time + ')' + RESET + '\\n';
|
|
3216
|
-
}
|
|
3217
|
-
}
|
|
3218
|
-
|
|
3219
|
-
out += '\\n' + DIM + 'Page ' + (Math.floor(offset / pageSize) + 1) + '/' + Math.ceil(sessions.length / pageSize) + ' (' + sessions.length + ' sessions)' + RESET;
|
|
3220
|
-
|
|
3221
|
-
process.stdout.write(out);
|
|
3222
|
-
}
|
|
3223
|
-
|
|
3224
|
-
async function main() {
|
|
3225
|
-
const sessions = collectSessions();
|
|
3226
|
-
|
|
3227
|
-
if (sessions.length === 0) {
|
|
3228
|
-
console.log(RED + 'No sessions with interactions found in current directory' + RESET);
|
|
3229
|
-
process.exit(0);
|
|
3230
|
-
}
|
|
3231
|
-
|
|
3232
|
-
if (!process.stdin.isTTY) {
|
|
3233
|
-
for (const s of sessions) {
|
|
3234
|
-
console.log(s.id + ' ' + (s.title || '') + ' ' + formatTime(s.lastTimestamp));
|
|
3235
|
-
}
|
|
3236
|
-
process.exit(0);
|
|
3237
|
-
}
|
|
3238
|
-
|
|
3239
|
-
const rows = process.stdout.rows || 24;
|
|
3240
|
-
const pageSize = rows - 6;
|
|
3241
|
-
let selected = 0;
|
|
3242
|
-
let offset = 0;
|
|
3243
|
-
|
|
3244
|
-
function restoreTerminal() {
|
|
3245
|
-
try { process.stdout.write(SHOW_CURSOR); } catch {}
|
|
3246
|
-
try { process.stdin.setRawMode(false); } catch {}
|
|
3247
|
-
try { process.stdin.pause(); } catch {}
|
|
3248
|
-
}
|
|
3249
|
-
|
|
3250
|
-
function clearScreen() {
|
|
3251
|
-
try { process.stdout.write(CLEAR); } catch {}
|
|
3252
|
-
}
|
|
3253
|
-
|
|
3254
|
-
process.stdin.setRawMode(true);
|
|
3255
|
-
process.stdin.resume();
|
|
3256
|
-
process.stdout.write(HIDE_CURSOR);
|
|
3257
|
-
|
|
3258
|
-
render(sessions, selected, offset, rows);
|
|
3259
|
-
|
|
3260
|
-
const onKey = (key) => {
|
|
3261
|
-
const k = key.toString();
|
|
3262
|
-
|
|
3263
|
-
if (k === 'q' || k === '\\x03') { // q or Ctrl+C
|
|
3264
|
-
restoreTerminal();
|
|
3265
|
-
clearScreen();
|
|
3266
|
-
process.exit(0);
|
|
3267
|
-
}
|
|
3268
|
-
|
|
3269
|
-
if (k === '\\r' || k === '\\n') { // Enter
|
|
3270
|
-
// Stop reading input / stop reacting to arrow keys before handing off to droid.
|
|
3271
|
-
process.stdin.off('data', onKey);
|
|
3272
|
-
restoreTerminal();
|
|
3273
|
-
clearScreen();
|
|
3274
|
-
const session = sessions[selected];
|
|
3275
|
-
console.log(GREEN + 'Resuming session: ' + session.id + RESET);
|
|
3276
|
-
console.log(DIM + 'Using: ' + ALIAS_NAME + ' --resume ' + session.id + RESET + '\\n');
|
|
3277
|
-
|
|
3278
|
-
// Avoid the sessions browser reacting to signals while droid is running.
|
|
3279
|
-
try { process.removeAllListeners('SIGINT'); } catch {}
|
|
3280
|
-
try { process.removeAllListeners('SIGTERM'); } catch {}
|
|
3281
|
-
try { process.on('SIGINT', () => {}); } catch {}
|
|
3282
|
-
try { process.on('SIGTERM', () => {}); } catch {}
|
|
3283
|
-
|
|
3284
|
-
const child = spawn(ALIAS_NAME, ['--resume', session.id], { stdio: 'inherit' });
|
|
3285
|
-
child.on('exit', (code) => process.exit(code || 0));
|
|
3286
|
-
child.on('error', () => process.exit(1));
|
|
3287
|
-
return;
|
|
3288
|
-
}
|
|
3289
|
-
|
|
3290
|
-
if (k === '\\x1b[A' || k === 'k') { // Up
|
|
3291
|
-
if (selected > 0) {
|
|
3292
|
-
selected--;
|
|
3293
|
-
if (selected < offset) offset = Math.max(0, offset - 1);
|
|
3294
|
-
}
|
|
3295
|
-
} else if (k === '\\x1b[B' || k === 'j') { // Down
|
|
3296
|
-
if (selected < sessions.length - 1) {
|
|
3297
|
-
selected++;
|
|
3298
|
-
if (selected >= offset + pageSize) offset++;
|
|
3299
|
-
}
|
|
3300
|
-
} else if (k === '\\x1b[5~') { // Page Up
|
|
3301
|
-
selected = Math.max(0, selected - pageSize);
|
|
3302
|
-
offset = Math.max(0, offset - pageSize);
|
|
3303
|
-
} else if (k === '\\x1b[6~') { // Page Down
|
|
3304
|
-
selected = Math.min(sessions.length - 1, selected + pageSize);
|
|
3305
|
-
offset = Math.min(Math.max(0, sessions.length - pageSize), offset + pageSize);
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
render(sessions, selected, offset, rows);
|
|
3309
|
-
};
|
|
3310
|
-
|
|
3311
|
-
process.stdin.on('data', onKey);
|
|
3312
|
-
|
|
3313
|
-
process.on('SIGINT', () => {
|
|
3314
|
-
restoreTerminal();
|
|
3315
|
-
clearScreen();
|
|
3316
|
-
process.exit(0);
|
|
3317
|
-
});
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
main();
|
|
3321
|
-
`;
|
|
3322
|
-
}
|
|
3323
|
-
/**
|
|
3324
|
-
* Create sessions browser script file
|
|
3325
|
-
*/
|
|
3326
|
-
async function createSessionsScript(outputDir, aliasName) {
|
|
3327
|
-
if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
|
|
3328
|
-
const sessionsScriptPath = join(outputDir, `${aliasName}-sessions.js`);
|
|
3329
|
-
await writeFile(sessionsScriptPath, generateSessionsBrowserScript(aliasName));
|
|
3330
|
-
await chmod(sessionsScriptPath, 493);
|
|
3331
|
-
return { sessionsScript: sessionsScriptPath };
|
|
3332
|
-
}
|
|
3333
|
-
|
|
3334
665
|
//#endregion
|
|
3335
666
|
//#region src/cli.ts
|
|
3336
667
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -3383,14 +714,12 @@ function findDefaultDroidPath() {
|
|
|
3383
714
|
for (const p of paths) if (existsSync(p)) return p;
|
|
3384
715
|
return join(home, ".droid", "bin", "droid");
|
|
3385
716
|
}
|
|
3386
|
-
bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy (each instance runs own proxy, auto-cleanup on exit)").option("--
|
|
717
|
+
bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy (each instance runs own proxy, auto-cleanup on exit)").option("--standalone", "Standalone mode: mock non-LLM Factory APIs (use with --websearch)").option("--reasoning-effort", "Enable reasoning effort for custom models (set to high, enable UI selector)").option("--disable-telemetry", "Disable telemetry and Sentry error reporting (block data uploads)").option("--auto-high", "Set default autonomy mode to auto-high (bypass settings.json race condition)").option("--dry-run", "Verify patches without actually modifying the binary").option("-p, --path <path>", "Path to the droid binary").option("-o, --output <dir>", "Output directory for patched binary").option("--no-backup", "Do not create backup of original binary").option("-v, --verbose", "Enable verbose output").argument("[alias]", "Alias name for the patched binary").action(async (options, args) => {
|
|
3387
718
|
const alias = args?.[0];
|
|
3388
719
|
const isCustom = options["is-custom"];
|
|
3389
720
|
const skipLogin = options["skip-login"];
|
|
3390
721
|
const apiBase = options["api-base"];
|
|
3391
722
|
const websearch = options["websearch"];
|
|
3392
|
-
const statusline = options["statusline"];
|
|
3393
|
-
const sessions = options["sessions"];
|
|
3394
723
|
const standalone = options["standalone"];
|
|
3395
724
|
const websearchTarget = websearch ? apiBase || "https://api.factory.ai" : void 0;
|
|
3396
725
|
const reasoningEffort = options["reasoning-effort"];
|
|
@@ -3402,13 +731,10 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3402
731
|
const backup = options.backup !== false;
|
|
3403
732
|
const verbose = options.verbose;
|
|
3404
733
|
const outputPath = outputDir && alias ? join(outputDir, alias) : void 0;
|
|
3405
|
-
|
|
3406
|
-
const statuslineEnabled = statusline;
|
|
3407
|
-
if (!needsBinaryPatch && (websearch || statuslineEnabled)) {
|
|
734
|
+
if (!(!!isCustom || !!skipLogin || !!reasoningEffort || !!noTelemetry || !!autoHigh || !!apiBase && !websearch) && websearch) {
|
|
3408
735
|
if (!alias) {
|
|
3409
|
-
console.log(styleText("red", "Error: Alias name required for --websearch
|
|
736
|
+
console.log(styleText("red", "Error: Alias name required for --websearch"));
|
|
3410
737
|
console.log(styleText("gray", "Usage: npx droid-patch --websearch <alias>"));
|
|
3411
|
-
console.log(styleText("gray", "Usage: npx droid-patch --statusline <alias>"));
|
|
3412
738
|
process.exit(1);
|
|
3413
739
|
}
|
|
3414
740
|
console.log(styleText("cyan", "═".repeat(60)));
|
|
@@ -3420,20 +746,10 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3420
746
|
console.log(styleText("white", `Forward target: ${websearchTarget}`));
|
|
3421
747
|
if (standalone) console.log(styleText("white", `Standalone mode: enabled`));
|
|
3422
748
|
}
|
|
3423
|
-
if (statuslineEnabled) console.log(styleText("white", `Statusline: enabled`));
|
|
3424
749
|
console.log();
|
|
3425
750
|
let execTargetPath = path;
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
execTargetPath = wrapperScript;
|
|
3429
|
-
}
|
|
3430
|
-
if (statuslineEnabled) {
|
|
3431
|
-
const statuslineDir = join(homedir(), ".droid-patch", "statusline");
|
|
3432
|
-
let sessionsScript;
|
|
3433
|
-
if (sessions) sessionsScript = (await createSessionsScript(statuslineDir, alias)).sessionsScript;
|
|
3434
|
-
const { wrapperScript } = await createStatuslineFiles(statuslineDir, execTargetPath, alias, sessionsScript);
|
|
3435
|
-
execTargetPath = wrapperScript;
|
|
3436
|
-
}
|
|
751
|
+
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchTarget, standalone);
|
|
752
|
+
execTargetPath = wrapperScript;
|
|
3437
753
|
const aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
|
|
3438
754
|
const droidVersion = getDroidVersion(path);
|
|
3439
755
|
await saveAliasMetadata(createMetadata(alias, path, {
|
|
@@ -3441,8 +757,6 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3441
757
|
skipLogin: false,
|
|
3442
758
|
apiBase: apiBase || null,
|
|
3443
759
|
websearch: !!websearch,
|
|
3444
|
-
statusline: !!statuslineEnabled,
|
|
3445
|
-
sessions: !!sessions,
|
|
3446
760
|
reasoningEffort: false,
|
|
3447
761
|
noTelemetry: false,
|
|
3448
762
|
standalone
|
|
@@ -3478,13 +792,12 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3478
792
|
}
|
|
3479
793
|
return;
|
|
3480
794
|
}
|
|
3481
|
-
if (!isCustom && !skipLogin && !apiBase && !websearch && !
|
|
795
|
+
if (!isCustom && !skipLogin && !apiBase && !websearch && !reasoningEffort && !noTelemetry && !autoHigh) {
|
|
3482
796
|
console.log(styleText("yellow", "No patch flags specified. Available patches:"));
|
|
3483
797
|
console.log(styleText("gray", " --is-custom Patch isCustom for custom models"));
|
|
3484
798
|
console.log(styleText("gray", " --skip-login Bypass login by injecting a fake API key"));
|
|
3485
799
|
console.log(styleText("gray", " --api-base Replace API URL (standalone: max 22 chars; with --websearch: no limit)"));
|
|
3486
800
|
console.log(styleText("gray", " --websearch Enable local WebSearch proxy"));
|
|
3487
|
-
console.log(styleText("gray", " --statusline Enable Claude-style statusline"));
|
|
3488
801
|
console.log(styleText("gray", " --reasoning-effort Set reasoning effort level for custom models"));
|
|
3489
802
|
console.log(styleText("gray", " --disable-telemetry Disable telemetry and Sentry error reporting"));
|
|
3490
803
|
console.log(styleText("gray", " --auto-high Set default autonomy mode to auto-high"));
|
|
@@ -3496,8 +809,6 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3496
809
|
console.log(styleText("cyan", " npx droid-patch --is-custom --skip-login droid-patched"));
|
|
3497
810
|
console.log(styleText("cyan", " npx droid-patch --websearch droid-search"));
|
|
3498
811
|
console.log(styleText("cyan", " npx droid-patch --websearch --standalone droid-local"));
|
|
3499
|
-
console.log(styleText("cyan", " npx droid-patch --statusline droid-status"));
|
|
3500
|
-
console.log(styleText("cyan", " npx droid-patch --websearch --statusline droid-search-ui"));
|
|
3501
812
|
console.log(styleText("cyan", " npx droid-patch --disable-telemetry droid-private"));
|
|
3502
813
|
console.log(styleText("cyan", " npx droid-patch --websearch --api-base=http://127.0.0.1:20002 my-droid"));
|
|
3503
814
|
process.exit(1);
|
|
@@ -3644,17 +955,8 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3644
955
|
console.log(styleText("white", ` Forward target: ${websearchTarget}`));
|
|
3645
956
|
if (standalone) console.log(styleText("white", ` Standalone mode: enabled`));
|
|
3646
957
|
}
|
|
3647
|
-
if (statuslineEnabled) {
|
|
3648
|
-
const statuslineDir = join(homedir(), ".droid-patch", "statusline");
|
|
3649
|
-
let sessionsScript;
|
|
3650
|
-
if (sessions) sessionsScript = (await createSessionsScript(statuslineDir, alias)).sessionsScript;
|
|
3651
|
-
const { wrapperScript } = await createStatuslineFiles(statuslineDir, execTargetPath, alias, sessionsScript);
|
|
3652
|
-
execTargetPath = wrapperScript;
|
|
3653
|
-
console.log();
|
|
3654
|
-
console.log(styleText("cyan", "Statusline enabled"));
|
|
3655
|
-
}
|
|
3656
958
|
let aliasResult;
|
|
3657
|
-
if (websearch
|
|
959
|
+
if (websearch) aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
|
|
3658
960
|
else aliasResult = await createAlias(result.outputPath, alias, verbose);
|
|
3659
961
|
const droidVersion = getDroidVersion(path);
|
|
3660
962
|
await saveAliasMetadata(createMetadata(alias, path, {
|
|
@@ -3662,8 +964,6 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3662
964
|
skipLogin: !!skipLogin,
|
|
3663
965
|
apiBase: apiBase || null,
|
|
3664
966
|
websearch: !!websearch,
|
|
3665
|
-
statusline: !!statuslineEnabled,
|
|
3666
|
-
sessions: !!sessions,
|
|
3667
967
|
reasoningEffort: !!reasoningEffort,
|
|
3668
968
|
noTelemetry: !!noTelemetry,
|
|
3669
969
|
standalone: !!standalone,
|
|
@@ -3688,11 +988,29 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3688
988
|
}
|
|
3689
989
|
}).command("list", "List all droid-patch aliases").action(async () => {
|
|
3690
990
|
await listAliases();
|
|
3691
|
-
}).command("remove", "Remove alias(es) by name or filter").argument("[alias-or-path]", "Alias name or file path to remove").option("--patch-version <version>", "Remove aliases created by this droid-patch version").option("--droid-version <version>", "Remove aliases for this droid version").option("--flag <flag>", "Remove aliases with this flag (is-custom, skip-login, websearch,
|
|
991
|
+
}).command("remove", "Remove alias(es) by name or filter").argument("[alias-or-path]", "Alias name or file path to remove").option("--patch-version <version>", "Remove aliases created by this droid-patch version").option("--droid-version <version>", "Remove aliases for this droid version").option("--flag <flag>", "Remove aliases with this flag (is-custom, skip-login, websearch, api-base, reasoning-effort, disable-telemetry, standalone)").action(async (options, args) => {
|
|
3692
992
|
const target = args?.[0];
|
|
3693
993
|
const patchVersion = options["patch-version"];
|
|
3694
994
|
const droidVersion = options["droid-version"];
|
|
3695
|
-
const
|
|
995
|
+
const flagRaw = options.flag;
|
|
996
|
+
let flag;
|
|
997
|
+
if (flagRaw) {
|
|
998
|
+
const allowedFlags = [
|
|
999
|
+
"is-custom",
|
|
1000
|
+
"skip-login",
|
|
1001
|
+
"websearch",
|
|
1002
|
+
"api-base",
|
|
1003
|
+
"reasoning-effort",
|
|
1004
|
+
"disable-telemetry",
|
|
1005
|
+
"standalone"
|
|
1006
|
+
];
|
|
1007
|
+
if (!allowedFlags.includes(flagRaw)) {
|
|
1008
|
+
console.error(styleText("red", `Error: Invalid --flag value: ${flagRaw}`));
|
|
1009
|
+
console.error(styleText("gray", `Allowed: ${allowedFlags.join(", ")}`));
|
|
1010
|
+
process.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
flag = flagRaw;
|
|
1013
|
+
}
|
|
3696
1014
|
if (patchVersion || droidVersion || flag) {
|
|
3697
1015
|
await removeAliasesByFilter({
|
|
3698
1016
|
patchVersion,
|
|
@@ -3886,14 +1204,8 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
3886
1204
|
delete meta.patches.proxy;
|
|
3887
1205
|
}
|
|
3888
1206
|
}
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
let sessionsScript;
|
|
3892
|
-
if (meta.patches.sessions) sessionsScript = (await createSessionsScript(statuslineDir, meta.name)).sessionsScript;
|
|
3893
|
-
const { wrapperScript } = await createStatuslineFiles(statuslineDir, execTargetPath, meta.name, sessionsScript);
|
|
3894
|
-
execTargetPath = wrapperScript;
|
|
3895
|
-
if (verbose) console.log(styleText("gray", ` Regenerated statusline wrapper`));
|
|
3896
|
-
}
|
|
1207
|
+
delete meta.patches.statusline;
|
|
1208
|
+
delete meta.patches.sessions;
|
|
3897
1209
|
const { symlink: symlink$1, unlink: unlink$1, readlink: readlink$1, lstat } = await import("node:fs/promises");
|
|
3898
1210
|
let aliasPath = meta.aliasPath;
|
|
3899
1211
|
if (!aliasPath) {
|