@raingor/pi-web-switch 0.4.4 → 0.6.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/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/pi-reader.ts +262 -0
- package/src/components/sessions/MemoryPage.tsx +292 -10
- package/src/components/settings/SettingsPage.tsx +0 -72
- package/src/lib/translations/en.ts +25 -0
- package/src/lib/translations/ja.ts +25 -0
- package/src/lib/translations/zh-CN.ts +25 -0
- package/src/lib/translations/zh-TW.ts +25 -0
- package/vite.config.ts +34 -0
package/dist/index.html
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
<link rel="manifest" href="./manifest.webmanifest" />
|
|
10
10
|
<meta name="theme-color" content="#05090d" />
|
|
11
11
|
<meta name="description" content="Web UI for pi coding agent — configuration management, session browser, and usage dashboard" />
|
|
12
|
-
<script type="module" crossorigin src="./assets/main-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="./assets/main-
|
|
12
|
+
<script type="module" crossorigin src="./assets/main-CqkZxauO.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="./assets/main-DiEs78Xc.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/pi-reader.ts
CHANGED
|
@@ -1633,6 +1633,268 @@ export function readSessionPreview(filePath: string, limit = 20): { messages: Se
|
|
|
1633
1633
|
|
|
1634
1634
|
const MEMORY_FILENAMES = ["MEMORY.md", "USER.md", "failures.md"];
|
|
1635
1635
|
|
|
1636
|
+
// ─── Hermes Memory Config (auto-write model + optimize) ──
|
|
1637
|
+
// The pi-hermes-memory extension reads ~/.pi/agent/hermes-memory-config.json
|
|
1638
|
+
// to decide which model performs automatic memory writes and consolidation
|
|
1639
|
+
// (`llmModelOverride`, `llmThinkingOverride`) and how long a consolidation run
|
|
1640
|
+
// may take (`consolidationTimeoutMs`). We read/merge-write just those keys so
|
|
1641
|
+
// unrelated fields the extension may add later are preserved.
|
|
1642
|
+
const HERMES_MEMORY_CONFIG_PATH = join(PI_DIR, "hermes-memory-config.json");
|
|
1643
|
+
const HERMES_EXTENSION_ENTRY = join(
|
|
1644
|
+
PI_DIR, "npm", "node_modules", "pi-hermes-memory", "src", "index.ts"
|
|
1645
|
+
);
|
|
1646
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
1647
|
+
type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
1648
|
+
const OVERFLOW_STRATEGIES = ["auto-consolidate", "reject", "fifo-evict"] as const;
|
|
1649
|
+
type OverflowStrategy = (typeof OVERFLOW_STRATEGIES)[number];
|
|
1650
|
+
|
|
1651
|
+
// Mirror pi-hermes-memory's own defaults (src/constants.ts) so the panel shows
|
|
1652
|
+
// the same effective limits the extension enforces when the keys are unset.
|
|
1653
|
+
const DEFAULT_MEMORY_CHAR_LIMIT = 5000;
|
|
1654
|
+
const DEFAULT_USER_CHAR_LIMIT = 5000;
|
|
1655
|
+
|
|
1656
|
+
export interface HermesMemoryConfig {
|
|
1657
|
+
llmModelOverride?: string;
|
|
1658
|
+
llmThinkingOverride?: ThinkingLevel;
|
|
1659
|
+
consolidationTimeoutMs?: number;
|
|
1660
|
+
memoryCharLimit?: number;
|
|
1661
|
+
userCharLimit?: number;
|
|
1662
|
+
memoryOverflowStrategy?: OverflowStrategy;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
export function readHermesMemoryConfig(): HermesMemoryConfig {
|
|
1666
|
+
try {
|
|
1667
|
+
if (!existsSync(HERMES_MEMORY_CONFIG_PATH)) return {};
|
|
1668
|
+
const parsed = JSON.parse(readFileSync(HERMES_MEMORY_CONFIG_PATH, "utf-8")) as Record<string, unknown>;
|
|
1669
|
+
const out: HermesMemoryConfig = {};
|
|
1670
|
+
if (typeof parsed.llmModelOverride === "string" && parsed.llmModelOverride.trim()) {
|
|
1671
|
+
out.llmModelOverride = parsed.llmModelOverride.trim();
|
|
1672
|
+
}
|
|
1673
|
+
if (typeof parsed.llmThinkingOverride === "string" && (THINKING_LEVELS as readonly string[]).includes(parsed.llmThinkingOverride)) {
|
|
1674
|
+
out.llmThinkingOverride = parsed.llmThinkingOverride as ThinkingLevel;
|
|
1675
|
+
}
|
|
1676
|
+
if (typeof parsed.consolidationTimeoutMs === "number" && Number.isFinite(parsed.consolidationTimeoutMs)) {
|
|
1677
|
+
out.consolidationTimeoutMs = parsed.consolidationTimeoutMs;
|
|
1678
|
+
}
|
|
1679
|
+
if (typeof parsed.memoryCharLimit === "number" && Number.isFinite(parsed.memoryCharLimit) && parsed.memoryCharLimit > 0) {
|
|
1680
|
+
out.memoryCharLimit = parsed.memoryCharLimit;
|
|
1681
|
+
}
|
|
1682
|
+
if (typeof parsed.userCharLimit === "number" && Number.isFinite(parsed.userCharLimit) && parsed.userCharLimit > 0) {
|
|
1683
|
+
out.userCharLimit = parsed.userCharLimit;
|
|
1684
|
+
}
|
|
1685
|
+
if (typeof parsed.memoryOverflowStrategy === "string" && (OVERFLOW_STRATEGIES as readonly string[]).includes(parsed.memoryOverflowStrategy)) {
|
|
1686
|
+
out.memoryOverflowStrategy = parsed.memoryOverflowStrategy as OverflowStrategy;
|
|
1687
|
+
}
|
|
1688
|
+
return out;
|
|
1689
|
+
} catch {
|
|
1690
|
+
return {};
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/** Merge-write the auto-write model settings, preserving any other keys. */
|
|
1695
|
+
export function writeHermesMemoryConfig(patch: HermesMemoryConfig): boolean {
|
|
1696
|
+
try {
|
|
1697
|
+
let existing: Record<string, unknown> = {};
|
|
1698
|
+
if (existsSync(HERMES_MEMORY_CONFIG_PATH)) {
|
|
1699
|
+
try {
|
|
1700
|
+
existing = JSON.parse(readFileSync(HERMES_MEMORY_CONFIG_PATH, "utf-8")) as Record<string, unknown>;
|
|
1701
|
+
} catch {
|
|
1702
|
+
existing = {};
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
// Empty string clears the override so the extension falls back to defaults.
|
|
1706
|
+
if (patch.llmModelOverride !== undefined) {
|
|
1707
|
+
const v = patch.llmModelOverride.trim();
|
|
1708
|
+
if (v) existing.llmModelOverride = v;
|
|
1709
|
+
else delete existing.llmModelOverride;
|
|
1710
|
+
}
|
|
1711
|
+
if (patch.llmThinkingOverride !== undefined) {
|
|
1712
|
+
if ((THINKING_LEVELS as readonly string[]).includes(patch.llmThinkingOverride)) {
|
|
1713
|
+
existing.llmThinkingOverride = patch.llmThinkingOverride;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
if (patch.consolidationTimeoutMs !== undefined && Number.isFinite(patch.consolidationTimeoutMs)) {
|
|
1717
|
+
existing.consolidationTimeoutMs = patch.consolidationTimeoutMs;
|
|
1718
|
+
}
|
|
1719
|
+
if (patch.memoryCharLimit !== undefined && Number.isFinite(patch.memoryCharLimit) && patch.memoryCharLimit > 0) {
|
|
1720
|
+
existing.memoryCharLimit = patch.memoryCharLimit;
|
|
1721
|
+
}
|
|
1722
|
+
if (patch.userCharLimit !== undefined && Number.isFinite(patch.userCharLimit) && patch.userCharLimit > 0) {
|
|
1723
|
+
existing.userCharLimit = patch.userCharLimit;
|
|
1724
|
+
}
|
|
1725
|
+
if (patch.memoryOverflowStrategy !== undefined && (OVERFLOW_STRATEGIES as readonly string[]).includes(patch.memoryOverflowStrategy)) {
|
|
1726
|
+
existing.memoryOverflowStrategy = patch.memoryOverflowStrategy;
|
|
1727
|
+
}
|
|
1728
|
+
writeFileSync(HERMES_MEMORY_CONFIG_PATH, JSON.stringify(existing, null, 2), "utf-8");
|
|
1729
|
+
return true;
|
|
1730
|
+
} catch {
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/**
|
|
1736
|
+
* Lightweight capacity snapshot for the memory page. Reports per-target usage
|
|
1737
|
+
* against the effective char limits the extension enforces (failure gets 2x the
|
|
1738
|
+
* memory limit). The extension measures capacity in CHARACTERS, not bytes, so
|
|
1739
|
+
* we read each file and use its character length — byte size (statSync) would
|
|
1740
|
+
* over-count ~3x for CJK text and show false "over limit" bars.
|
|
1741
|
+
*/
|
|
1742
|
+
export function readMemoryStatus(): {
|
|
1743
|
+
targets: { filename: string; target: "memory" | "user" | "failure"; chars: number; limit: number }[];
|
|
1744
|
+
} {
|
|
1745
|
+
const cfg = readHermesMemoryConfig();
|
|
1746
|
+
const memLimit = cfg.memoryCharLimit ?? DEFAULT_MEMORY_CHAR_LIMIT;
|
|
1747
|
+
const userLimit = cfg.userCharLimit ?? DEFAULT_USER_CHAR_LIMIT;
|
|
1748
|
+
const map: { filename: string; target: "memory" | "user" | "failure"; limit: number }[] = [
|
|
1749
|
+
{ filename: "MEMORY.md", target: "memory", limit: memLimit },
|
|
1750
|
+
{ filename: "USER.md", target: "user", limit: userLimit },
|
|
1751
|
+
{ filename: "failures.md", target: "failure", limit: memLimit * 2 },
|
|
1752
|
+
];
|
|
1753
|
+
return {
|
|
1754
|
+
targets: map.map(({ filename, target, limit }) => {
|
|
1755
|
+
const p = join(HERMES_DIR, filename);
|
|
1756
|
+
let chars = 0;
|
|
1757
|
+
try {
|
|
1758
|
+
chars = existsSync(p) ? readFileSync(p, "utf-8").length : 0;
|
|
1759
|
+
} catch {
|
|
1760
|
+
chars = 0;
|
|
1761
|
+
}
|
|
1762
|
+
return { filename, target, chars, limit };
|
|
1763
|
+
}),
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/** Resolve a usable `pi` binary: PI_BINARY env → PATH → known install dirs. */
|
|
1768
|
+
function resolvePiBin(): string | null {
|
|
1769
|
+
const home = homedir();
|
|
1770
|
+
const candidates = [
|
|
1771
|
+
process.env.PI_BINARY,
|
|
1772
|
+
"pi",
|
|
1773
|
+
`${home}/.npm-global/bin/pi`,
|
|
1774
|
+
`${home}/.local/share/pnpm/pi`,
|
|
1775
|
+
].filter(Boolean) as string[];
|
|
1776
|
+
for (const bin of candidates) {
|
|
1777
|
+
try {
|
|
1778
|
+
const out = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 15000 });
|
|
1779
|
+
if (out.status === 0 && out.stdout.trim()) return bin;
|
|
1780
|
+
} catch {
|
|
1781
|
+
// try next
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
// Fall back to `which pi`
|
|
1785
|
+
try {
|
|
1786
|
+
const which = spawnSync("which", ["pi"], { encoding: "utf8", timeout: 5000 });
|
|
1787
|
+
if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();
|
|
1788
|
+
} catch {
|
|
1789
|
+
// ignore
|
|
1790
|
+
}
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function memoryFileSizes(): Record<string, number> {
|
|
1795
|
+
const sizes: Record<string, number> = {};
|
|
1796
|
+
for (const name of MEMORY_FILENAMES) {
|
|
1797
|
+
const p = join(HERMES_DIR, name);
|
|
1798
|
+
try {
|
|
1799
|
+
sizes[name] = existsSync(p) ? statSync(p).size : 0;
|
|
1800
|
+
} catch {
|
|
1801
|
+
sizes[name] = 0;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return sizes;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
export interface OptimizeMemoryResult {
|
|
1808
|
+
success: boolean;
|
|
1809
|
+
before: Record<string, number>;
|
|
1810
|
+
after: Record<string, number>;
|
|
1811
|
+
freedBytes: number;
|
|
1812
|
+
message?: string;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/**
|
|
1816
|
+
* One-click memory optimization. Runs pi's `/memory-consolidate` command in a
|
|
1817
|
+
* headless child process with the pi-hermes-memory extension loaded, using the
|
|
1818
|
+
* configured auto-write model override. Reports the byte delta across the three
|
|
1819
|
+
* memory files. Long-running (the command spawns an LLM turn), so the caller
|
|
1820
|
+
* should surface a spinner.
|
|
1821
|
+
*/
|
|
1822
|
+
export async function optimizeMemory(): Promise<OptimizeMemoryResult> {
|
|
1823
|
+
const before = memoryFileSizes();
|
|
1824
|
+
const bin = resolvePiBin();
|
|
1825
|
+
if (!bin) {
|
|
1826
|
+
return { success: false, before, after: before, freedBytes: 0, message: "pi binary not found" };
|
|
1827
|
+
}
|
|
1828
|
+
if (!existsSync(HERMES_EXTENSION_ENTRY)) {
|
|
1829
|
+
return { success: false, before, after: before, freedBytes: 0, message: "pi-hermes-memory extension not found" };
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const cfg = readHermesMemoryConfig();
|
|
1833
|
+
const args = ["-p", "--no-session", "-e", HERMES_EXTENSION_ENTRY];
|
|
1834
|
+
if (cfg.llmModelOverride) args.push("--model", cfg.llmModelOverride);
|
|
1835
|
+
args.push("--thinking", cfg.llmThinkingOverride ?? "off");
|
|
1836
|
+
|
|
1837
|
+
// Compute per-target capacity so the prompt can name the over-limit targets
|
|
1838
|
+
// explicitly. Weak/free models are conservative and skip merging when told
|
|
1839
|
+
// only "merge duplicates"; giving them a concrete goal ("USER is at 105%,
|
|
1840
|
+
// get it under the limit") is what actually makes them shrink memory.
|
|
1841
|
+
const status = readMemoryStatus();
|
|
1842
|
+
const capacityLines = status.targets.map((tg) => {
|
|
1843
|
+
const pct = tg.limit > 0 ? Math.round((tg.chars / tg.limit) * 100) : 0;
|
|
1844
|
+
const over = tg.chars > tg.limit;
|
|
1845
|
+
return `- target "${tg.target}" (${tg.filename}): ${tg.chars}/${tg.limit} chars (${pct}%)${over ? " — OVER LIMIT, must shrink below the limit" : ""}`;
|
|
1846
|
+
});
|
|
1847
|
+
const overTargets = status.targets.filter((tg) => tg.chars > tg.limit).map((tg) => `"${tg.target}"`);
|
|
1848
|
+
|
|
1849
|
+
// Direct consolidation prompt rather than the /memory-consolidate slash
|
|
1850
|
+
// command: the slash command spawns a *nested* child pi process per target
|
|
1851
|
+
// (memory/user/failure/project), which routinely runs for 10+ minutes.
|
|
1852
|
+
// Driving the memory tools directly in this single child is far faster.
|
|
1853
|
+
args.push(
|
|
1854
|
+
[
|
|
1855
|
+
"Consolidate my long-term memory to reduce redundancy WITHOUT losing important facts.",
|
|
1856
|
+
"Current capacity per target:",
|
|
1857
|
+
...capacityLines,
|
|
1858
|
+
overTargets.length > 0
|
|
1859
|
+
? `The following targets are OVER their limit and MUST be reduced below it: ${overTargets.join(", ")}. This is the primary goal — you must actually remove or merge entries so each over-limit target ends up under its char limit.`
|
|
1860
|
+
: "No target is over its limit; still merge any obvious duplicates and drop clearly stale entries.",
|
|
1861
|
+
"For EACH target: call memory_search (with an empty or broad query) to list its current entries, then use memory_remove to drop outdated/superseded/duplicate entries and memory_replace/memory_add to merge related entries into fewer, more concise ones.",
|
|
1862
|
+
"Always preserve user preferences and explicit corrections (highest priority). Prefer merging several similar entries into one tight entry over deleting unique facts.",
|
|
1863
|
+
"Do not stop until every over-limit target is under its limit. When finished, reply with a one-line summary of what changed per target.",
|
|
1864
|
+
].join("\n")
|
|
1865
|
+
);
|
|
1866
|
+
|
|
1867
|
+
const timeoutMs = cfg.consolidationTimeoutMs && cfg.consolidationTimeoutMs > 0
|
|
1868
|
+
? cfg.consolidationTimeoutMs
|
|
1869
|
+
: 600000;
|
|
1870
|
+
|
|
1871
|
+
try {
|
|
1872
|
+
const out = spawnSync(bin, args, {
|
|
1873
|
+
encoding: "utf8",
|
|
1874
|
+
timeout: timeoutMs,
|
|
1875
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
1876
|
+
});
|
|
1877
|
+
const after = memoryFileSizes();
|
|
1878
|
+
const freedBytes = Object.values(before).reduce((a, b) => a + b, 0)
|
|
1879
|
+
- Object.values(after).reduce((a, b) => a + b, 0);
|
|
1880
|
+
if (out.error) {
|
|
1881
|
+
const killed = (out.error as NodeJS.ErrnoException).code === "ETIMEDOUT";
|
|
1882
|
+
return {
|
|
1883
|
+
success: false,
|
|
1884
|
+
before,
|
|
1885
|
+
after,
|
|
1886
|
+
freedBytes,
|
|
1887
|
+
message: killed ? `consolidation timed out after ${Math.round(timeoutMs / 1000)}s` : String(out.error.message),
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
return { success: out.status === 0, before, after, freedBytes, message: out.status === 0 ? undefined : `exit ${out.status}` };
|
|
1891
|
+
} catch (e) {
|
|
1892
|
+
const after = memoryFileSizes();
|
|
1893
|
+
return { success: false, before, after, freedBytes: 0, message: e instanceof Error ? e.message : String(e) };
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
|
|
1636
1898
|
/** Strip the trailing `<!-- created=..., last=... -->` marker from a § section. */
|
|
1637
1899
|
function sectionText(section: string): string {
|
|
1638
1900
|
return section.replace(/<!--\s*created\s*=[^>]*-->\s*$/, "").trim();
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
Trash2,
|
|
17
17
|
ChevronDown,
|
|
18
18
|
ChevronRight,
|
|
19
|
+
Sparkles,
|
|
20
|
+
Settings2,
|
|
19
21
|
} from "lucide-react";
|
|
20
22
|
|
|
21
23
|
interface MemoryFile {
|
|
@@ -370,7 +372,7 @@ function MemoryFileSection({
|
|
|
370
372
|
|
|
371
373
|
export function MemoryPage() {
|
|
372
374
|
const { t } = useTranslation();
|
|
373
|
-
const { initialized } = useConfigStore();
|
|
375
|
+
const { initialized, allModels } = useConfigStore();
|
|
374
376
|
const [files, setFiles] = useState<MemoryFile[]>([]);
|
|
375
377
|
const [loading, setLoading] = useState(true);
|
|
376
378
|
const [refreshing, setRefreshing] = useState(false);
|
|
@@ -379,6 +381,28 @@ export function MemoryPage() {
|
|
|
379
381
|
const [deleteTarget, setDeleteTarget] = useState<{ filename: string; entry: MemoryEntry } | null>(null);
|
|
380
382
|
const [deleting, setDeleting] = useState(false);
|
|
381
383
|
|
|
384
|
+
// One-click optimize (memory consolidation)
|
|
385
|
+
const [optimizing, setOptimizing] = useState(false);
|
|
386
|
+
const [optimizeMsg, setOptimizeMsg] = useState<{ ok: boolean; text: string } | null>(null);
|
|
387
|
+
const [optimizeElapsed, setOptimizeElapsed] = useState(0);
|
|
388
|
+
|
|
389
|
+
// Auto-write memory model config
|
|
390
|
+
const [showConfig, setShowConfig] = useState(false);
|
|
391
|
+
const [cfg, setCfg] = useState<{ llmModelOverride: string; llmThinkingOverride: string; consolidationTimeoutMs: number; memoryCharLimit: number; userCharLimit: number; memoryOverflowStrategy: string }>(
|
|
392
|
+
{ llmModelOverride: "", llmThinkingOverride: "off", consolidationTimeoutMs: 600000, memoryCharLimit: 5000, userCharLimit: 5000, memoryOverflowStrategy: "auto-consolidate" }
|
|
393
|
+
);
|
|
394
|
+
const [cfgSaving, setCfgSaving] = useState(false);
|
|
395
|
+
const [cfgMsg, setCfgMsg] = useState<{ ok: boolean; text: string } | null>(null);
|
|
396
|
+
|
|
397
|
+
// Capacity status (per-target chars vs limit) for progress bars
|
|
398
|
+
const [status, setStatus] = useState<{ filename: string; target: string; chars: number; limit: number }[]>([]);
|
|
399
|
+
const loadStatus = useCallback(() => {
|
|
400
|
+
fetch("/api/pi/memory/status")
|
|
401
|
+
.then((r) => r.json())
|
|
402
|
+
.then((s: { targets?: { filename: string; target: string; chars: number; limit: number }[] }) => setStatus(s.targets ?? []))
|
|
403
|
+
.catch(() => {});
|
|
404
|
+
}, []);
|
|
405
|
+
|
|
382
406
|
const loadAll = useCallback(() => {
|
|
383
407
|
if (!initialized) return;
|
|
384
408
|
setRefreshing(true);
|
|
@@ -396,6 +420,86 @@ export function MemoryPage() {
|
|
|
396
420
|
}, [initialized]);
|
|
397
421
|
|
|
398
422
|
useEffect(() => { loadAll(); }, [loadAll]);
|
|
423
|
+
useEffect(() => { loadStatus(); }, [loadStatus]);
|
|
424
|
+
|
|
425
|
+
// Load the hermes auto-write model config once.
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
fetch("/api/pi/memory/config")
|
|
428
|
+
.then((r) => r.json())
|
|
429
|
+
.then((c: { llmModelOverride?: string; llmThinkingOverride?: string; consolidationTimeoutMs?: number; memoryCharLimit?: number; userCharLimit?: number; memoryOverflowStrategy?: string }) =>
|
|
430
|
+
setCfg({
|
|
431
|
+
llmModelOverride: c.llmModelOverride ?? "",
|
|
432
|
+
llmThinkingOverride: c.llmThinkingOverride ?? "off",
|
|
433
|
+
consolidationTimeoutMs: c.consolidationTimeoutMs ?? 600000,
|
|
434
|
+
memoryCharLimit: c.memoryCharLimit ?? 5000,
|
|
435
|
+
userCharLimit: c.userCharLimit ?? 5000,
|
|
436
|
+
memoryOverflowStrategy: c.memoryOverflowStrategy ?? "auto-consolidate",
|
|
437
|
+
})
|
|
438
|
+
)
|
|
439
|
+
.catch(() => {});
|
|
440
|
+
}, []);
|
|
441
|
+
|
|
442
|
+
const formatBytes = (n: number) => {
|
|
443
|
+
const abs = Math.abs(n);
|
|
444
|
+
if (abs >= 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
445
|
+
return `${n} B`;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const handleOptimize = async () => {
|
|
449
|
+
setOptimizing(true);
|
|
450
|
+
setOptimizeMsg(null);
|
|
451
|
+
setOptimizeElapsed(0);
|
|
452
|
+
// While the consolidation child runs, poll capacity + elapsed so the user
|
|
453
|
+
// sees the bars move and a running timer instead of a frozen spinner.
|
|
454
|
+
const startedAt = Date.now();
|
|
455
|
+
const timer = window.setInterval(() => {
|
|
456
|
+
setOptimizeElapsed(Math.round((Date.now() - startedAt) / 1000));
|
|
457
|
+
loadStatus();
|
|
458
|
+
}, 2000);
|
|
459
|
+
try {
|
|
460
|
+
const res = await fetch("/api/pi/memory/optimize", { method: "POST" });
|
|
461
|
+
const result = (await res.json()) as { success: boolean; freedBytes?: number; message?: string };
|
|
462
|
+
if (result.success) {
|
|
463
|
+
const freed = result.freedBytes ?? 0;
|
|
464
|
+
setOptimizeMsg({ ok: true, text: freed > 0 ? t("memory.optimize_done", formatBytes(freed)) : t("memory.optimize_none") });
|
|
465
|
+
loadAll();
|
|
466
|
+
loadStatus();
|
|
467
|
+
} else {
|
|
468
|
+
setOptimizeMsg({ ok: false, text: t("memory.optimize_failed", result.message ?? "") });
|
|
469
|
+
}
|
|
470
|
+
} catch (e) {
|
|
471
|
+
setOptimizeMsg({ ok: false, text: t("memory.optimize_failed", e instanceof Error ? e.message : "") });
|
|
472
|
+
} finally {
|
|
473
|
+
window.clearInterval(timer);
|
|
474
|
+
setOptimizing(false);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const handleSaveConfig = async () => {
|
|
479
|
+
setCfgSaving(true);
|
|
480
|
+
setCfgMsg(null);
|
|
481
|
+
try {
|
|
482
|
+
const res = await fetch("/api/pi/memory/config", {
|
|
483
|
+
method: "POST",
|
|
484
|
+
headers: { "Content-Type": "application/json" },
|
|
485
|
+
body: JSON.stringify({
|
|
486
|
+
llmModelOverride: cfg.llmModelOverride.trim(),
|
|
487
|
+
llmThinkingOverride: cfg.llmThinkingOverride,
|
|
488
|
+
consolidationTimeoutMs: cfg.consolidationTimeoutMs,
|
|
489
|
+
memoryCharLimit: cfg.memoryCharLimit,
|
|
490
|
+
userCharLimit: cfg.userCharLimit,
|
|
491
|
+
memoryOverflowStrategy: cfg.memoryOverflowStrategy,
|
|
492
|
+
}),
|
|
493
|
+
});
|
|
494
|
+
const { success } = (await res.json()) as { success: boolean };
|
|
495
|
+
setCfgMsg({ ok: !!success, text: success ? t("memory.config_saved") : t("memory.config_save_failed") });
|
|
496
|
+
if (success) loadStatus();
|
|
497
|
+
} catch {
|
|
498
|
+
setCfgMsg({ ok: false, text: t("memory.config_save_failed") });
|
|
499
|
+
} finally {
|
|
500
|
+
setCfgSaving(false);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
399
503
|
|
|
400
504
|
const handleDelete = async () => {
|
|
401
505
|
if (!deleteTarget) return;
|
|
@@ -472,17 +576,78 @@ export function MemoryPage() {
|
|
|
472
576
|
{t("memory.summary", String(totalEntries), String(parsed.length))}
|
|
473
577
|
</p>
|
|
474
578
|
</div>
|
|
475
|
-
<
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
579
|
+
<div className="flex items-center gap-2">
|
|
580
|
+
<button
|
|
581
|
+
onClick={handleOptimize}
|
|
582
|
+
disabled={optimizing || parsed.length === 0}
|
|
583
|
+
className="flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors disabled:opacity-50"
|
|
584
|
+
style={{ borderColor: "var(--card-border)", color: "var(--muted-text)", backgroundColor: "var(--card-bg)" }}
|
|
585
|
+
title={t("memory.optimize_hint")}
|
|
586
|
+
>
|
|
587
|
+
<Sparkles className={optimizing ? "h-3.5 w-3.5 animate-pulse" : "h-3.5 w-3.5"} />
|
|
588
|
+
{optimizing ? t("memory.optimizing") : t("memory.optimize")}
|
|
589
|
+
</button>
|
|
590
|
+
<button
|
|
591
|
+
onClick={() => { setCfgMsg(null); setShowConfig(true); }}
|
|
592
|
+
className="flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors"
|
|
593
|
+
style={{ borderColor: "var(--card-border)", color: "var(--muted-text)", backgroundColor: "var(--card-bg)" }}
|
|
594
|
+
title={t("memory.config")}
|
|
595
|
+
>
|
|
596
|
+
<Settings2 className="h-3.5 w-3.5" />
|
|
597
|
+
{t("memory.config")}
|
|
598
|
+
</button>
|
|
599
|
+
<button
|
|
600
|
+
onClick={loadAll}
|
|
601
|
+
className="flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors"
|
|
602
|
+
style={{ borderColor: "var(--card-border)", color: "var(--muted-text)", backgroundColor: "var(--card-bg)" }}
|
|
603
|
+
title={t("memory.refresh")}
|
|
604
|
+
>
|
|
605
|
+
<RefreshCw className={refreshing ? "h-3.5 w-3.5 animate-spin" : "h-3.5 w-3.5"} />
|
|
606
|
+
{t("memory.refresh")}
|
|
607
|
+
</button>
|
|
608
|
+
</div>
|
|
484
609
|
</div>
|
|
485
610
|
|
|
611
|
+
{(optimizing || optimizeMsg || status.length > 0) && (
|
|
612
|
+
<div className="tech-panel space-y-3 rounded-xl border px-4 py-3" style={{ borderColor: "var(--card-border)", backgroundColor: "var(--card-bg)" }}>
|
|
613
|
+
{/* Per-target capacity bars */}
|
|
614
|
+
<div className="space-y-2">
|
|
615
|
+
{status.map((s) => {
|
|
616
|
+
const pct = s.limit > 0 ? Math.min(100, Math.round((s.chars / s.limit) * 100)) : 0;
|
|
617
|
+
const over = s.chars > s.limit;
|
|
618
|
+
const barColor = over ? "#ef4444" : pct > 80 ? "#f59e0b" : "#10b981";
|
|
619
|
+
const label = FILE_LABEL_KEYS[s.filename] ? t(FILE_LABEL_KEYS[s.filename]!) : s.filename;
|
|
620
|
+
return (
|
|
621
|
+
<div key={s.filename}>
|
|
622
|
+
<div className="mb-1 flex items-center justify-between text-[11px]" style={{ color: "var(--muted-text)" }}>
|
|
623
|
+
<span>{label} <span style={{ color: "var(--subtle-text)" }}>· {s.filename}</span></span>
|
|
624
|
+
<span style={{ color: over ? "#ef4444" : "var(--muted-text)" }}>
|
|
625
|
+
{s.chars} / {s.limit} ({pct}%)
|
|
626
|
+
</span>
|
|
627
|
+
</div>
|
|
628
|
+
<div className="h-2 w-full overflow-hidden rounded-full" style={{ backgroundColor: "var(--page-bg)" }}>
|
|
629
|
+
<div className="h-full rounded-full transition-all duration-500" style={{ width: `${pct}%`, backgroundColor: barColor }} />
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
);
|
|
633
|
+
})}
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
{/* Optimize progress / result line */}
|
|
637
|
+
{optimizing && (
|
|
638
|
+
<div className="flex items-center gap-2 text-xs" style={{ color: "var(--muted-text)" }}>
|
|
639
|
+
<div className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-gray-500 border-t-transparent" />
|
|
640
|
+
<span>{t("memory.optimizing")} · {optimizeElapsed}s</span>
|
|
641
|
+
</div>
|
|
642
|
+
)}
|
|
643
|
+
{!optimizing && optimizeMsg && (
|
|
644
|
+
<div className="text-xs" style={{ color: optimizeMsg.ok ? "#10b981" : "#ef4444" }}>
|
|
645
|
+
{optimizeMsg.text}
|
|
646
|
+
</div>
|
|
647
|
+
)}
|
|
648
|
+
</div>
|
|
649
|
+
)}
|
|
650
|
+
|
|
486
651
|
{parsed.length === 0 ? (
|
|
487
652
|
<div className="memory-empty-state">
|
|
488
653
|
<div className="memory-empty-radar"><Brain className="h-7 w-7" /></div>
|
|
@@ -548,6 +713,123 @@ export function MemoryPage() {
|
|
|
548
713
|
</>
|
|
549
714
|
)}
|
|
550
715
|
|
|
716
|
+
{/* Auto-write memory model config */}
|
|
717
|
+
<Modal
|
|
718
|
+
open={showConfig}
|
|
719
|
+
onClose={() => !cfgSaving && setShowConfig(false)}
|
|
720
|
+
title={t("memory.config_title")}
|
|
721
|
+
size="md"
|
|
722
|
+
>
|
|
723
|
+
<div className="space-y-4">
|
|
724
|
+
<p className="text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_desc")}</p>
|
|
725
|
+
<div>
|
|
726
|
+
<label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_model")}</label>
|
|
727
|
+
<input
|
|
728
|
+
type="text"
|
|
729
|
+
list="memory-model-options"
|
|
730
|
+
value={cfg.llmModelOverride}
|
|
731
|
+
onChange={(e) => setCfg({ ...cfg, llmModelOverride: e.target.value })}
|
|
732
|
+
placeholder={t("memory.config_model_default")}
|
|
733
|
+
className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
|
|
734
|
+
style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
|
|
735
|
+
/>
|
|
736
|
+
<datalist id="memory-model-options">
|
|
737
|
+
{allModels.map((m) => (
|
|
738
|
+
<option key={`${m.providerId}/${m.id}`} value={`${m.providerId}/${m.id}`}>{m.providerName} · {m.name ?? m.id}</option>
|
|
739
|
+
))}
|
|
740
|
+
</datalist>
|
|
741
|
+
</div>
|
|
742
|
+
<div className="flex gap-3">
|
|
743
|
+
<div className="flex-1">
|
|
744
|
+
<label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_thinking")}</label>
|
|
745
|
+
<select
|
|
746
|
+
value={cfg.llmThinkingOverride}
|
|
747
|
+
onChange={(e) => setCfg({ ...cfg, llmThinkingOverride: e.target.value })}
|
|
748
|
+
className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
|
|
749
|
+
style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
|
|
750
|
+
>
|
|
751
|
+
{["off", "minimal", "low", "medium", "high", "xhigh"].map((lvl) => (
|
|
752
|
+
<option key={lvl} value={lvl}>{lvl}</option>
|
|
753
|
+
))}
|
|
754
|
+
</select>
|
|
755
|
+
</div>
|
|
756
|
+
<div className="flex-1">
|
|
757
|
+
<label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_timeout")}</label>
|
|
758
|
+
<input
|
|
759
|
+
type="number"
|
|
760
|
+
min={60}
|
|
761
|
+
value={Math.round(cfg.consolidationTimeoutMs / 1000)}
|
|
762
|
+
onChange={(e) => setCfg({ ...cfg, consolidationTimeoutMs: Math.max(60, Number(e.target.value) || 0) * 1000 })}
|
|
763
|
+
className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
|
|
764
|
+
style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
|
|
765
|
+
/>
|
|
766
|
+
</div>
|
|
767
|
+
</div>
|
|
768
|
+
{/* Capacity limits */}
|
|
769
|
+
<div className="flex gap-3">
|
|
770
|
+
<div className="flex-1">
|
|
771
|
+
<label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_mem_limit")}</label>
|
|
772
|
+
<input
|
|
773
|
+
type="number"
|
|
774
|
+
min={1000}
|
|
775
|
+
step={500}
|
|
776
|
+
value={cfg.memoryCharLimit}
|
|
777
|
+
onChange={(e) => setCfg({ ...cfg, memoryCharLimit: Math.max(1000, Number(e.target.value) || 0) })}
|
|
778
|
+
className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
|
|
779
|
+
style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
|
|
780
|
+
/>
|
|
781
|
+
<p className="mt-1 text-[10px]" style={{ color: "var(--subtle-text)" }}>{t("memory.config_mem_limit_hint", String(cfg.memoryCharLimit * 2))}</p>
|
|
782
|
+
</div>
|
|
783
|
+
<div className="flex-1">
|
|
784
|
+
<label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_user_limit")}</label>
|
|
785
|
+
<input
|
|
786
|
+
type="number"
|
|
787
|
+
min={1000}
|
|
788
|
+
step={500}
|
|
789
|
+
value={cfg.userCharLimit}
|
|
790
|
+
onChange={(e) => setCfg({ ...cfg, userCharLimit: Math.max(1000, Number(e.target.value) || 0) })}
|
|
791
|
+
className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
|
|
792
|
+
style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
|
|
793
|
+
/>
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
<div>
|
|
797
|
+
<label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_overflow")}</label>
|
|
798
|
+
<select
|
|
799
|
+
value={cfg.memoryOverflowStrategy}
|
|
800
|
+
onChange={(e) => setCfg({ ...cfg, memoryOverflowStrategy: e.target.value })}
|
|
801
|
+
className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
|
|
802
|
+
style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
|
|
803
|
+
>
|
|
804
|
+
<option value="auto-consolidate">{t("memory.config_overflow_auto")}</option>
|
|
805
|
+
<option value="reject">{t("memory.config_overflow_reject")}</option>
|
|
806
|
+
<option value="fifo-evict">{t("memory.config_overflow_fifo")}</option>
|
|
807
|
+
</select>
|
|
808
|
+
</div>
|
|
809
|
+
<div className="flex items-center justify-end gap-3">
|
|
810
|
+
{cfgMsg && (
|
|
811
|
+
<span className="text-xs" style={{ color: cfgMsg.ok ? "#10b981" : "#ef4444" }}>{cfgMsg.text}</span>
|
|
812
|
+
)}
|
|
813
|
+
<button
|
|
814
|
+
onClick={() => setShowConfig(false)}
|
|
815
|
+
disabled={cfgSaving}
|
|
816
|
+
className="rounded-lg border px-4 py-2 text-sm"
|
|
817
|
+
style={{ borderColor: "var(--card-border)", color: "var(--muted-text)" }}
|
|
818
|
+
>
|
|
819
|
+
{t("memory.config_cancel")}
|
|
820
|
+
</button>
|
|
821
|
+
<button
|
|
822
|
+
onClick={handleSaveConfig}
|
|
823
|
+
disabled={cfgSaving}
|
|
824
|
+
className="rounded-lg px-4 py-2 text-sm font-medium text-white disabled:opacity-50"
|
|
825
|
+
style={{ backgroundColor: "#2563eb" }}
|
|
826
|
+
>
|
|
827
|
+
{cfgSaving ? t("memory.config_saving") : t("memory.config_save")}
|
|
828
|
+
</button>
|
|
829
|
+
</div>
|
|
830
|
+
</div>
|
|
831
|
+
</Modal>
|
|
832
|
+
|
|
551
833
|
{/* Delete entry confirm */}
|
|
552
834
|
<Modal
|
|
553
835
|
open={deleteTarget !== null}
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
CloudDownload,
|
|
28
28
|
RefreshCw,
|
|
29
29
|
ZoomIn,
|
|
30
|
-
KeyRound,
|
|
31
30
|
} from "lucide-react";
|
|
32
31
|
|
|
33
32
|
type SettingsTab = "appearance" | "models" | "advanced";
|
|
@@ -117,37 +116,6 @@ export function SettingsPage() {
|
|
|
117
116
|
const [updateError, setUpdateError] = useState(false);
|
|
118
117
|
const [applying, setApplying] = useState(false);
|
|
119
118
|
const [applyMessage, setApplyMessage] = useState<{ ok: number; failNames: string[] } | null>(null);
|
|
120
|
-
// GitHub Copilot usage API config (username + classic PAT).
|
|
121
|
-
const [copilotCfg, setCopilotCfg] = useState<{ username: string; token: string }>({ username: "", token: "" });
|
|
122
|
-
const [copilotSaving, setCopilotSaving] = useState(false);
|
|
123
|
-
const [copilotMsg, setCopilotMsg] = useState<{ ok: boolean; text: string } | null>(null);
|
|
124
|
-
|
|
125
|
-
useEffect(() => {
|
|
126
|
-
fetch("/api/pi/copilot-config")
|
|
127
|
-
.then((r) => r.json())
|
|
128
|
-
.then((cfg: { username?: string; token?: string }) =>
|
|
129
|
-
setCopilotCfg({ username: cfg.username ?? "", token: cfg.token ?? "" })
|
|
130
|
-
)
|
|
131
|
-
.catch(() => {});
|
|
132
|
-
}, []);
|
|
133
|
-
|
|
134
|
-
const handleSaveCopilot = async () => {
|
|
135
|
-
setCopilotSaving(true);
|
|
136
|
-
setCopilotMsg(null);
|
|
137
|
-
try {
|
|
138
|
-
const res = await fetch("/api/pi/copilot-config", {
|
|
139
|
-
method: "POST",
|
|
140
|
-
headers: { "Content-Type": "application/json" },
|
|
141
|
-
body: JSON.stringify(copilotCfg),
|
|
142
|
-
});
|
|
143
|
-
const { success } = (await res.json()) as { success: boolean };
|
|
144
|
-
setCopilotMsg({ ok: !!success, text: success ? t("settings.copilot_saved") : t("settings.copilot_save_failed") });
|
|
145
|
-
} catch {
|
|
146
|
-
setCopilotMsg({ ok: false, text: t("settings.copilot_save_failed") });
|
|
147
|
-
} finally {
|
|
148
|
-
setCopilotSaving(false);
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
119
|
|
|
152
120
|
const handleCheckUpdates = async () => {
|
|
153
121
|
setCheckingUpdates(true);
|
|
@@ -600,46 +568,6 @@ export function SettingsPage() {
|
|
|
600
568
|
})()}
|
|
601
569
|
</Card>
|
|
602
570
|
|
|
603
|
-
<Card icon={KeyRound} title={t("settings.copilot_title")} desc={t("settings.copilot_desc")}>
|
|
604
|
-
<div className="max-w-md space-y-3">
|
|
605
|
-
<div>
|
|
606
|
-
<label className="mb-1 block text-xs text-gray-500">{t("settings.copilot_username")}</label>
|
|
607
|
-
<input
|
|
608
|
-
type="text"
|
|
609
|
-
value={copilotCfg.username}
|
|
610
|
-
onChange={(e) => setCopilotCfg({ ...copilotCfg, username: e.target.value })}
|
|
611
|
-
placeholder="octocat"
|
|
612
|
-
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-white placeholder-gray-600"
|
|
613
|
-
/>
|
|
614
|
-
</div>
|
|
615
|
-
<div>
|
|
616
|
-
<label className="mb-1 block text-xs text-gray-500">{t("settings.copilot_token")}</label>
|
|
617
|
-
<input
|
|
618
|
-
type="password"
|
|
619
|
-
value={copilotCfg.token}
|
|
620
|
-
onChange={(e) => setCopilotCfg({ ...copilotCfg, token: e.target.value })}
|
|
621
|
-
placeholder="ghp_…"
|
|
622
|
-
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-white placeholder-gray-600"
|
|
623
|
-
/>
|
|
624
|
-
</div>
|
|
625
|
-
<div className="flex items-center gap-3">
|
|
626
|
-
<button
|
|
627
|
-
onClick={handleSaveCopilot}
|
|
628
|
-
disabled={copilotSaving}
|
|
629
|
-
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-4 py-1.5 text-sm text-gray-300 hover:bg-gray-700 disabled:opacity-60"
|
|
630
|
-
>
|
|
631
|
-
{copilotSaving ? t("settings.copilot_saving") : t("settings.copilot_save")}
|
|
632
|
-
</button>
|
|
633
|
-
{copilotMsg && (
|
|
634
|
-
<span className={cn("text-xs", copilotMsg.ok ? "text-emerald-400" : "text-red-400")}>
|
|
635
|
-
{copilotMsg.text}
|
|
636
|
-
</span>
|
|
637
|
-
)}
|
|
638
|
-
</div>
|
|
639
|
-
<p className="text-[11px] leading-relaxed text-gray-500">{t("settings.copilot_token_hint")}</p>
|
|
640
|
-
</div>
|
|
641
|
-
</Card>
|
|
642
|
-
|
|
643
571
|
<Card icon={Package} title={t("settings.packages")}>
|
|
644
572
|
{(settings?.packages ?? []).length > 0 && (
|
|
645
573
|
<div className="mb-4 flex flex-wrap gap-1.5">
|
|
@@ -365,6 +365,31 @@ const en: Record<string, string> = {
|
|
|
365
365
|
"memory.channels": "Memory Channels",
|
|
366
366
|
"memory.knowledge_synced": "Knowledge Base // Synced",
|
|
367
367
|
"memory.files_indexed_detail": "{0} files · {1} memories",
|
|
368
|
+
"memory.optimize": "Optimize Memory",
|
|
369
|
+
"memory.optimizing": "Optimizing…",
|
|
370
|
+
"memory.optimize_done": "Optimized · freed {0}",
|
|
371
|
+
"memory.optimize_none": "Optimized · nothing to trim",
|
|
372
|
+
"memory.optimize_failed": "Optimize failed: {0}",
|
|
373
|
+
"memory.optimize_hint": "Runs pi's memory consolidation to merge duplicates and drop stale entries",
|
|
374
|
+
"memory.config": "Memory Model",
|
|
375
|
+
"memory.config_title": "Model for automatic memory writes",
|
|
376
|
+
"memory.config_desc": "Automatic memory writes and consolidation run on this model (blank = default)",
|
|
377
|
+
"memory.config_model": "Model",
|
|
378
|
+
"memory.config_model_default": "(default / unset)",
|
|
379
|
+
"memory.config_thinking": "Thinking level",
|
|
380
|
+
"memory.config_timeout": "Consolidation timeout (s)",
|
|
381
|
+
"memory.config_save": "Save",
|
|
382
|
+
"memory.config_saving": "Saving…",
|
|
383
|
+
"memory.config_saved": "Saved",
|
|
384
|
+
"memory.config_save_failed": "Save failed",
|
|
385
|
+
"memory.config_cancel": "Cancel",
|
|
386
|
+
"memory.config_mem_limit": "Project memory limit (chars)",
|
|
387
|
+
"memory.config_mem_limit_hint": "Failure records get 2x: {0}",
|
|
388
|
+
"memory.config_user_limit": "User profile limit (chars)",
|
|
389
|
+
"memory.config_overflow": "Overflow strategy",
|
|
390
|
+
"memory.config_overflow_auto": "Auto-consolidate (recommended)",
|
|
391
|
+
"memory.config_overflow_reject": "Reject writes",
|
|
392
|
+
"memory.config_overflow_fifo": "Evict oldest",
|
|
368
393
|
|
|
369
394
|
"common.today": "Today",
|
|
370
395
|
"common.yesterday": "Yesterday",
|
|
@@ -358,6 +358,31 @@ const ja: Record<string, string> = {
|
|
|
358
358
|
"memory.channels": "メモリーチャンネル",
|
|
359
359
|
"memory.knowledge_synced": "ナレッジベース // 同期済み",
|
|
360
360
|
"memory.files_indexed_detail": "{0} ファイル · {1} メモリー",
|
|
361
|
+
"memory.optimize": "メモリを最適化",
|
|
362
|
+
"memory.optimizing": "最適化中…",
|
|
363
|
+
"memory.optimize_done": "最適化完了 · {0} 解放",
|
|
364
|
+
"memory.optimize_none": "最適化完了 · 整理不要",
|
|
365
|
+
"memory.optimize_failed": "最適化失敗:{0}",
|
|
366
|
+
"memory.optimize_hint": "pi のメモリ統合を実行し、重複の統合と古い項目の削除を行います",
|
|
367
|
+
"memory.config": "メモリモデル設定",
|
|
368
|
+
"memory.config_title": "自動メモリ書き込みに使うモデル",
|
|
369
|
+
"memory.config_desc": "自動メモリ書き込みと統合はこのモデルで実行されます(空欄なら既定)",
|
|
370
|
+
"memory.config_model": "モデル",
|
|
371
|
+
"memory.config_model_default": "(既定 / 未指定)",
|
|
372
|
+
"memory.config_thinking": "思考レベル",
|
|
373
|
+
"memory.config_timeout": "統合タイムアウト(秒)",
|
|
374
|
+
"memory.config_save": "保存",
|
|
375
|
+
"memory.config_saving": "保存中…",
|
|
376
|
+
"memory.config_saved": "保存しました",
|
|
377
|
+
"memory.config_save_failed": "保存に失敗",
|
|
378
|
+
"memory.config_cancel": "キャンセル",
|
|
379
|
+
"memory.config_mem_limit": "プロジェクト記憶上限(文字)",
|
|
380
|
+
"memory.config_mem_limit_hint": "障害記録は2倍:{0}",
|
|
381
|
+
"memory.config_user_limit": "ユーザープロファイル上限(文字)",
|
|
382
|
+
"memory.config_overflow": "上限超過時の戦略",
|
|
383
|
+
"memory.config_overflow_auto": "自動統合(推奨)",
|
|
384
|
+
"memory.config_overflow_reject": "書き込み拒否",
|
|
385
|
+
"memory.config_overflow_fifo": "最古を削除",
|
|
361
386
|
|
|
362
387
|
"common.today": "今日",
|
|
363
388
|
"common.yesterday": "昨日",
|
|
@@ -358,6 +358,31 @@ const zhCN: Record<string, string> = {
|
|
|
358
358
|
"memory.channels": "记忆频道",
|
|
359
359
|
"memory.knowledge_synced": "知识库 // 已同步",
|
|
360
360
|
"memory.files_indexed_detail": "{0} 个文件 · {1} 条记忆",
|
|
361
|
+
"memory.optimize": "一键优化记忆",
|
|
362
|
+
"memory.optimizing": "优化中…",
|
|
363
|
+
"memory.optimize_done": "优化完成 · 释放 {0}",
|
|
364
|
+
"memory.optimize_none": "优化完成 · 无需精简",
|
|
365
|
+
"memory.optimize_failed": "优化失败:{0}",
|
|
366
|
+
"memory.optimize_hint": "调用 pi 的记忆整合,合并重复条目、清理过时记忆",
|
|
367
|
+
"memory.config": "记忆模型配置",
|
|
368
|
+
"memory.config_title": "自动写入记忆所用模型",
|
|
369
|
+
"memory.config_desc": "自动记忆写入与整合由该模型执行(留空则用默认)",
|
|
370
|
+
"memory.config_model": "模型",
|
|
371
|
+
"memory.config_model_default": "(默认 / 不指定)",
|
|
372
|
+
"memory.config_thinking": "思考等级",
|
|
373
|
+
"memory.config_timeout": "整合超时(秒)",
|
|
374
|
+
"memory.config_save": "保存",
|
|
375
|
+
"memory.config_saving": "保存中…",
|
|
376
|
+
"memory.config_saved": "已保存",
|
|
377
|
+
"memory.config_save_failed": "保存失败",
|
|
378
|
+
"memory.config_cancel": "取消",
|
|
379
|
+
"memory.config_mem_limit": "项目记忆上限(字符)",
|
|
380
|
+
"memory.config_mem_limit_hint": "故障记录上限为其 2 倍:{0}",
|
|
381
|
+
"memory.config_user_limit": "用户画像上限(字符)",
|
|
382
|
+
"memory.config_overflow": "超限策略",
|
|
383
|
+
"memory.config_overflow_auto": "自动整合(推荐)",
|
|
384
|
+
"memory.config_overflow_reject": "拒绝写入",
|
|
385
|
+
"memory.config_overflow_fifo": "淘汰最旧",
|
|
361
386
|
|
|
362
387
|
"common.today": "今天",
|
|
363
388
|
"common.yesterday": "昨天",
|
|
@@ -357,6 +357,31 @@ const zhTW: Record<string, string> = {
|
|
|
357
357
|
"memory.channels": "記憶頻道",
|
|
358
358
|
"memory.knowledge_synced": "知識庫 // 已同步",
|
|
359
359
|
"memory.files_indexed_detail": "{0} 個檔案 · {1} 條記憶",
|
|
360
|
+
"memory.optimize": "一鍵優化記憶",
|
|
361
|
+
"memory.optimizing": "優化中…",
|
|
362
|
+
"memory.optimize_done": "優化完成 · 釋放 {0}",
|
|
363
|
+
"memory.optimize_none": "優化完成 · 無需精簡",
|
|
364
|
+
"memory.optimize_failed": "優化失敗:{0}",
|
|
365
|
+
"memory.optimize_hint": "呼叫 pi 的記憶整合,合併重複條目、清理過時記憶",
|
|
366
|
+
"memory.config": "記憶模型設定",
|
|
367
|
+
"memory.config_title": "自動寫入記憶所用模型",
|
|
368
|
+
"memory.config_desc": "自動記憶寫入與整合由該模型執行(留空則用預設)",
|
|
369
|
+
"memory.config_model": "模型",
|
|
370
|
+
"memory.config_model_default": "(預設 / 不指定)",
|
|
371
|
+
"memory.config_thinking": "思考等級",
|
|
372
|
+
"memory.config_timeout": "整合逾時(秒)",
|
|
373
|
+
"memory.config_save": "儲存",
|
|
374
|
+
"memory.config_saving": "儲存中…",
|
|
375
|
+
"memory.config_saved": "已儲存",
|
|
376
|
+
"memory.config_save_failed": "儲存失敗",
|
|
377
|
+
"memory.config_cancel": "取消",
|
|
378
|
+
"memory.config_mem_limit": "專案記憶上限(字元)",
|
|
379
|
+
"memory.config_mem_limit_hint": "故障記錄上限為其 2 倍:{0}",
|
|
380
|
+
"memory.config_user_limit": "使用者畫像上限(字元)",
|
|
381
|
+
"memory.config_overflow": "超限策略",
|
|
382
|
+
"memory.config_overflow_auto": "自動整合(推薦)",
|
|
383
|
+
"memory.config_overflow_reject": "拒絕寫入",
|
|
384
|
+
"memory.config_overflow_fifo": "淘汰最舊",
|
|
360
385
|
|
|
361
386
|
"common.today": "今天",
|
|
362
387
|
"common.yesterday": "昨天",
|
package/vite.config.ts
CHANGED
|
@@ -148,6 +148,40 @@ function piApiPlugin(): Plugin {
|
|
|
148
148
|
res.setHeader("Content-Type", "application/json");
|
|
149
149
|
res.end(JSON.stringify(data));
|
|
150
150
|
},
|
|
151
|
+
"GET /api/pi/memory/config"(_, res) {
|
|
152
|
+
res.setHeader("Content-Type", "application/json");
|
|
153
|
+
res.end(JSON.stringify(pi.readHermesMemoryConfig() ?? {}));
|
|
154
|
+
},
|
|
155
|
+
"GET /api/pi/memory/status"(_, res) {
|
|
156
|
+
res.setHeader("Content-Type", "application/json");
|
|
157
|
+
res.end(JSON.stringify(pi.readMemoryStatus()));
|
|
158
|
+
},
|
|
159
|
+
"POST /api/pi/memory/config"(req, res) {
|
|
160
|
+
let body = "";
|
|
161
|
+
req.on("data", (chunk: string) => (body += chunk));
|
|
162
|
+
req.on("end", () => {
|
|
163
|
+
try {
|
|
164
|
+
const patch = JSON.parse(body);
|
|
165
|
+
const ok = pi.writeHermesMemoryConfig(patch);
|
|
166
|
+
res.setHeader("Content-Type", "application/json");
|
|
167
|
+
res.end(JSON.stringify({ success: ok }));
|
|
168
|
+
} catch {
|
|
169
|
+
res.statusCode = 400;
|
|
170
|
+
res.setHeader("Content-Type", "application/json");
|
|
171
|
+
res.end(JSON.stringify({ success: false, error: "Invalid request body" }));
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
"POST /api/pi/memory/optimize"(_, res) {
|
|
176
|
+
pi.optimizeMemory().then((result: unknown) => {
|
|
177
|
+
res.setHeader("Content-Type", "application/json");
|
|
178
|
+
res.end(JSON.stringify(result));
|
|
179
|
+
}).catch((error: unknown) => {
|
|
180
|
+
res.statusCode = 500;
|
|
181
|
+
res.setHeader("Content-Type", "application/json");
|
|
182
|
+
res.end(JSON.stringify({ success: false, error: error instanceof Error ? error.message : "Optimize failed" }));
|
|
183
|
+
});
|
|
184
|
+
},
|
|
151
185
|
"POST /api/pi/memory/delete-entry"(req, res) {
|
|
152
186
|
let body = "";
|
|
153
187
|
req.on("data", (chunk: string) => (body += chunk));
|