@litfamily/litclaude 1.0.24 → 1.0.25
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/CHANGELOG.md +8 -0
- package/README.md +8 -8
- package/README_ko-KR.md +8 -8
- package/bin/litclaude-ai.js +90 -1
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/hooks.md +17 -8
- package/package.json +1 -1
- package/plugins/litclaude/.claude-plugin/plugin.json +2 -2
- package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
- package/plugins/litclaude/hooks/lit-band-client.mjs +20 -13
- package/plugins/litclaude/lib/mod/frame.mjs +60 -21
- package/plugins/litclaude/lib/mod/motion.mjs +11 -5
- package/plugins/litclaude/lib/mod/settings.mjs +4 -4
- package/plugins/litclaude/lib/mod/timeline.mjs +28 -0
- package/plugins/litclaude/lib/mod/view.mjs +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.25 - 2026-10-12
|
|
4
|
+
|
|
5
|
+
- The LIT card now moves while LitClaude is at work, so a glance tells you it is busy. The LIT pill and the border slowly glow from pink to flame orange and back, a light passes over the current phase and the skill chips of this request, and a spark runs along the top edge and back along the bottom one. When the turn ends the card goes still again. With the `motion` setting at `subtle` only the glow stays, and `off` keeps the card still.
|
|
6
|
+
- The card is wider. It now grows with the terminal up to 120 columns instead of 80.
|
|
7
|
+
- The bottom edge shows the weekly usage window next to the five-hour one, as the status line does.
|
|
8
|
+
- The installer sets up the Claude desktop app too. The app's Code tab reads the same settings as the terminal but has no editor for the card's options, so the installer now writes the defaults for `motion`, `glyphs` and `band`, keeping any value you already chose. The new `litclaude-ai config` command shows them, and `litclaude-ai config <option> <value>` changes one from a terminal. Uninstall removes only the defaults it wrote that you never changed.
|
|
9
|
+
- The README demo film now opens on a title card and names each step with a numbered two-line caption, and the card pictures show the new width and the weekly window.
|
|
10
|
+
|
|
3
11
|
## 1.0.24 - 2026-10-12
|
|
4
12
|
|
|
5
13
|
- The LIT card no longer drops to idle while background work is still running. When the main turn ends after starting background subagents or a workflow, the card stays at work and counts the running agents until the last one finishes, and it never reads `DONE` while an agent is still busy. A workflow's agents now count too, and a subagent that finishes in the middle of a turn no longer dims the chips of the request that is still running. An agent that never reports back stops counting after 30 quiet minutes, so it cannot hold the card at work.
|
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/assets/cover-motion.webp" width="100%" alt="LitFamily motion cover: five armored robots power on one by one, the LitClaude robot wakes with glowing eyes and a lit frame, then LITFAMILY and KEEP THE WORK LIT. light up." /></picture></p>
|
|
2
2
|
|
|
3
3
|
<h1 align="center">LitClaude</h1>
|
|
4
4
|
<p align="center"><strong>Keep the work lit.</strong></p>
|
|
5
5
|
<p align="center">A Claude Code plugin that helps every session plan the work, build it, check it, and leave a note the next session can pick up.</p>
|
|
6
6
|
<p align="center">
|
|
7
|
-
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
8
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
7
|
+
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/assets/readme/badge-version.svg" alt="1.0.25" />
|
|
8
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
<p align="center">
|
|
11
|
-
<a href="https://github.com/wjgoarxiv/litclaude#readme">Full guide and skills gallery on GitHub</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
11
|
+
<a href="https://github.com/wjgoarxiv/litclaude#readme">Full guide and skills gallery on GitHub</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/README_ko-KR.md">한국어</a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
Add `lit` to a Claude Code prompt and Claude pins the goal, writes a failing test first,
|
|
@@ -73,7 +73,7 @@ the `/plugin` menu.
|
|
|
73
73
|
## More
|
|
74
74
|
|
|
75
75
|
- [Full guide and skills gallery on GitHub](https://github.com/wjgoarxiv/litclaude#readme)
|
|
76
|
-
- [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
77
|
-
- [Migration and ownership conflicts](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
78
|
-
- [Release history](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
79
|
-
- [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
76
|
+
- [Privacy and network behavior](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/privacy.md)
|
|
77
|
+
- [Migration and ownership conflicts](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/migration.md)
|
|
78
|
+
- [Release history](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/CHANGELOG.md)
|
|
79
|
+
- [MIT license](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/LICENSE)
|
package/README_ko-KR.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
1
|
+
<p align="center"><picture><source media="(prefers-reduced-motion: reduce)" srcset="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/assets/cover-motion-still.webp" /><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/assets/cover-motion.webp" width="100%" alt="LitFamily 모션 커버: 다섯 로봇 패널이 차례로 켜지고, LitClaude 로봇의 눈과 테두리가 빛난 뒤 LITFAMILY와 KEEP THE WORK LIT. 문구가 밝아지는 영상" /></picture></p>
|
|
2
2
|
|
|
3
3
|
<h1 align="center">LitClaude</h1>
|
|
4
4
|
<p align="center"><strong>Keep the work lit.</strong></p>
|
|
5
5
|
<p align="center">Claude Code에서 세션마다 계획하고, 만들고, 확인한 뒤 다음 세션이 이어받을 기록을 남기게 돕는 플러그인입니다.</p>
|
|
6
6
|
<p align="center">
|
|
7
|
-
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
8
|
-
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
7
|
+
<img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/assets/readme/badge-version.svg" alt="1.0.25" />
|
|
8
|
+
<a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/LICENSE"><img src="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/assets/readme/badge-license.svg" alt="MIT license" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
<p align="center">
|
|
11
|
-
<a href="https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md">전체 안내와 스킬 갤러리는 GitHub에서</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
11
|
+
<a href="https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md">전체 안내와 스킬 갤러리는 GitHub에서</a> · <a href="https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/README.md">English</a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
Claude Code 프롬프트에 `lit`을 붙이면 Claude가 목표를 고정하고, 실패하는 테스트를 먼저 쓰고,
|
|
@@ -71,7 +71,7 @@ LitClaude는 프로젝트 상태를 `.litclaude/`에 두고, 그 안의 `.gitign
|
|
|
71
71
|
## 더 보기
|
|
72
72
|
|
|
73
73
|
- [전체 안내와 스킬 갤러리 (GitHub)](https://github.com/wjgoarxiv/litclaude/blob/main/README_ko-KR.md)
|
|
74
|
-
- [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
75
|
-
- [이전과 소유권 충돌](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
76
|
-
- [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
77
|
-
- [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.
|
|
74
|
+
- [개인정보와 네트워크 동작](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/privacy.md)
|
|
75
|
+
- [이전과 소유권 충돌](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/docs/migration.md)
|
|
76
|
+
- [변경 이력](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/CHANGELOG.md)
|
|
77
|
+
- [MIT 라이선스](https://cdn.jsdelivr.net/npm/@litfamily/litclaude@1.0.25/LICENSE)
|
package/bin/litclaude-ai.js
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
themeForAccent,
|
|
31
31
|
} from "../plugins/litclaude/lib/hud-accent-palette.mjs";
|
|
32
32
|
import { readPublicSource } from "../plugins/litclaude/lib/public-source-reader/reader.mjs";
|
|
33
|
+
import { MOD_CHOICES } from "../plugins/litclaude/lib/mod/settings.mjs";
|
|
33
34
|
import { verifyBundledSkillsIntegrity } from "../plugins/litclaude/lib/bundled-skills-integrity.mjs";
|
|
34
35
|
import { verifyCanonicalFrontendCorpus } from "../plugins/litclaude/lib/canonical-frontend-corpus.mjs";
|
|
35
36
|
import { verifyCanonicalRuntimeClosures } from "../plugins/litclaude/lib/canonical-runtime-closures.mjs";
|
|
@@ -80,12 +81,16 @@ const prewarmMotionRuntime = () => {
|
|
|
80
81
|
};
|
|
81
82
|
const version = packageJson.version;
|
|
82
83
|
|
|
83
|
-
const usage = `Usage: litclaude-ai [--dry-run] <install|doctor|path|run|update|uninstall|litgoal|wikify|workflow-check|start-work|start-work-next|public-read|motion-runtime> [...args]
|
|
84
|
+
const usage = `Usage: litclaude-ai [--dry-run] <install|doctor|config|path|run|update|uninstall|litgoal|wikify|workflow-check|start-work|start-work-next|public-read|motion-runtime> [...args]
|
|
84
85
|
litclaude-ai --version
|
|
85
86
|
|
|
86
87
|
Commands:
|
|
87
88
|
install Register the packaged LitClaude plugin in Claude Code.
|
|
88
89
|
doctor Validate the installed LitClaude plugin path.
|
|
90
|
+
config [<option> <value>]
|
|
91
|
+
Show or change the LIT card options (motion, glyphs, band) in
|
|
92
|
+
pluginConfigs, the same values /config edits. Works for the
|
|
93
|
+
desktop app too, which shows no editor for them.
|
|
89
94
|
path Print the installed Claude plugin path.
|
|
90
95
|
run -- ... Run Claude Code after the global plugin install.
|
|
91
96
|
litgoal Manage litgoal runtime state and evidence.
|
|
@@ -720,6 +725,82 @@ const registerMarketplace = (marketplacePath, home = claudeHome()) => {
|
|
|
720
725
|
writeKnownMarketplaces(knownMarketplaces, home);
|
|
721
726
|
};
|
|
722
727
|
|
|
728
|
+
// The LIT card's options live in settings.json `pluginConfigs`, which the terminal and the desktop
|
|
729
|
+
// app both read; the desktop app has no editor for them, so install writes each default the user
|
|
730
|
+
// has not set and records which ones it wrote. Uninstall removes only those still at that value.
|
|
731
|
+
const modOptionDefaults = () => Object.fromEntries(Object.keys(MOD_CHOICES).map((key) => [key, MOD_CHOICES[key][0]]));
|
|
732
|
+
const modOptionsOf = (settings) => ({ ...modOptionDefaults(), ...(settings.pluginConfigs?.[pluginKey]?.options ?? {}) });
|
|
733
|
+
const modOptionsLine = (settings) => {
|
|
734
|
+
const options = modOptionsOf(settings);
|
|
735
|
+
return Object.keys(MOD_CHOICES).map((key) => `${key} ${options[key]}`).join(" · ");
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const writeModOptions = (settings, options) => {
|
|
739
|
+
settings.pluginConfigs = settings.pluginConfigs ?? {};
|
|
740
|
+
const entry = settings.pluginConfigs[pluginKey] ?? {};
|
|
741
|
+
settings.pluginConfigs[pluginKey] = { ...entry, options: { ...(entry.options ?? {}), ...options } };
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const installModOptions = (home = claudeHome()) => {
|
|
745
|
+
const settings = readClaudeSettings(home);
|
|
746
|
+
const present = settings.pluginConfigs?.[pluginKey]?.options ?? {};
|
|
747
|
+
const existingLitClaude = settings[litClaudeSettingsKey] ?? {};
|
|
748
|
+
const written = { ...(existingLitClaude.modOptionsWritten ?? {}) };
|
|
749
|
+
const missing = Object.fromEntries(Object.entries(modOptionDefaults()).filter(([key]) => present[key] === undefined));
|
|
750
|
+
if (Object.keys(missing).length === 0) return;
|
|
751
|
+
writeModOptions(settings, missing);
|
|
752
|
+
settings[litClaudeSettingsKey] = { ...existingLitClaude, modOptionsWritten: { ...written, ...missing } };
|
|
753
|
+
writeClaudeSettings(settings, home);
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const uninstallModOptions = (home = claudeHome()) => {
|
|
757
|
+
if (!existsSync(claudeSettingsPath(home))) return;
|
|
758
|
+
const settings = readClaudeSettings(home);
|
|
759
|
+
const litClaude = settings[litClaudeSettingsKey];
|
|
760
|
+
if (!litClaude?.modOptionsWritten) return;
|
|
761
|
+
const entry = settings.pluginConfigs?.[pluginKey];
|
|
762
|
+
if (entry?.options) {
|
|
763
|
+
const options = { ...entry.options };
|
|
764
|
+
for (const [key, value] of Object.entries(litClaude.modOptionsWritten)) if (options[key] === value) delete options[key];
|
|
765
|
+
const rest = { ...entry };
|
|
766
|
+
if (Object.keys(options).length > 0) rest.options = options;
|
|
767
|
+
else delete rest.options;
|
|
768
|
+
if (Object.keys(rest).length > 0) settings.pluginConfigs[pluginKey] = rest;
|
|
769
|
+
else delete settings.pluginConfigs[pluginKey];
|
|
770
|
+
if (Object.keys(settings.pluginConfigs).length === 0) delete settings.pluginConfigs;
|
|
771
|
+
}
|
|
772
|
+
const remainingLitClaude = { ...litClaude };
|
|
773
|
+
delete remainingLitClaude.modOptionsWritten;
|
|
774
|
+
if (Object.keys(remainingLitClaude).length > 0) settings[litClaudeSettingsKey] = remainingLitClaude;
|
|
775
|
+
else delete settings[litClaudeSettingsKey];
|
|
776
|
+
writeClaudeSettings(settings, home);
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// `config` prints the options; `config <option> <value>` sets one. A value the user sets is theirs,
|
|
780
|
+
// so it leaves the install record and uninstall keeps it.
|
|
781
|
+
const configModOptions = (rest, home = claudeHome()) => {
|
|
782
|
+
if (rest.length === 0) {
|
|
783
|
+
const options = modOptionsOf(readClaudeSettings(home));
|
|
784
|
+
for (const key of Object.keys(MOD_CHOICES)) process.stdout.write(`${key.padEnd(7)} ${options[key].padEnd(7)} (${MOD_CHOICES[key].join(", ")})\n`);
|
|
785
|
+
process.stdout.write("Change one with: litclaude-ai config <option> <value>. New Claude Code sessions, terminal or desktop app, pick it up.\n");
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const [key, raw] = rest;
|
|
789
|
+
if (!Object.hasOwn(MOD_CHOICES, key)) fail(`Unknown option: ${key}. Options: ${Object.keys(MOD_CHOICES).join(", ")}`, 64);
|
|
790
|
+
const value = String(raw ?? "").trim().toLowerCase();
|
|
791
|
+
if (!MOD_CHOICES[key].includes(value) || rest.length > 2) fail(`${key} takes one of: ${MOD_CHOICES[key].join(", ")}`, 64);
|
|
792
|
+
const settings = readClaudeSettings(home);
|
|
793
|
+
writeModOptions(settings, { [key]: value });
|
|
794
|
+
const litClaude = settings[litClaudeSettingsKey];
|
|
795
|
+
if (litClaude?.modOptionsWritten && Object.hasOwn(litClaude.modOptionsWritten, key)) {
|
|
796
|
+
const written = { ...litClaude.modOptionsWritten };
|
|
797
|
+
delete written[key];
|
|
798
|
+
settings[litClaudeSettingsKey] = { ...litClaude, modOptionsWritten: written };
|
|
799
|
+
}
|
|
800
|
+
writeClaudeSettings(settings, home);
|
|
801
|
+
process.stdout.write(`MOD_OPTIONS: ${modOptionsLine(settings)}\n`);
|
|
802
|
+
};
|
|
803
|
+
|
|
723
804
|
const ownsHudStatusLine = (settings, ownership) =>
|
|
724
805
|
settings.statusLine?.type === "command" && settings.statusLine.command === ownership.statusLineCommand &&
|
|
725
806
|
Object.keys(settings.statusLine).every((key) => key === "type" || key === "command");
|
|
@@ -1025,6 +1106,7 @@ const registerClaudePlugin = (installRoot, marketplacePath, home = claudeHome(),
|
|
|
1025
1106
|
writeInstalledPlugins(registry, home);
|
|
1026
1107
|
registerMarketplace(marketplacePath, home);
|
|
1027
1108
|
installHudStatusLine(installRoot, home, hudAccent);
|
|
1109
|
+
installModOptions(home);
|
|
1028
1110
|
recordPermissionConfiguration(permissionMode, home);
|
|
1029
1111
|
};
|
|
1030
1112
|
|
|
@@ -1384,6 +1466,8 @@ const install = async ({ dryRun, rest }) => {
|
|
|
1384
1466
|
process.stdout.write(`Plugin path: ${intendedPluginPath(home)}\n`);
|
|
1385
1467
|
process.stdout.write(hudManaged ? `HUD: LitClaude statusLine installed (${hudAccent})\n` : "HUD: user statusLine setting preserved\n");
|
|
1386
1468
|
process.stdout.write(`Global Claude settings permissions.allow/deny: ${permissionMode} — ${permissionProfile(permissionMode).description}\n`);
|
|
1469
|
+
process.stdout.write(`MOD_OPTIONS: ${modOptionsLine(readClaudeSettings(home))} (change: litclaude-ai config <option> <value>)\n`);
|
|
1470
|
+
process.stdout.write("DESKTOP_APP: ready — the Claude desktop app's Code tab reads these same settings; open a new session there\n");
|
|
1387
1471
|
process.stdout.write(`${prewarmMotionRuntime()}\n`);
|
|
1388
1472
|
if (wantsIconFont) await runIconFontInstall();
|
|
1389
1473
|
process.stdout.write("Launch with: claude\n");
|
|
@@ -1505,6 +1589,7 @@ const doctor = ({ dryRun }) => {
|
|
|
1505
1589
|
process.stdout.write(`${jevStatusLine(process.env)}\n`);
|
|
1506
1590
|
writeAutoHandoffDoctorLines();
|
|
1507
1591
|
process.stdout.write(`${iconFontDoctorLine()}\n`);
|
|
1592
|
+
process.stdout.write(`MOD_OPTIONS: ${modOptionsLine(readClaudeSettings())}\n`);
|
|
1508
1593
|
|
|
1509
1594
|
// lit-typographic-motion: the five readiness probes, every run. Read-only and never fatal; a
|
|
1510
1595
|
// missing pre-warm names the command that fixes it.
|
|
@@ -1635,6 +1720,7 @@ const uninstall = ({ dryRun }) => {
|
|
|
1635
1720
|
uninstallHudStatusLine();
|
|
1636
1721
|
uninstallOutputStyle();
|
|
1637
1722
|
uninstallPermissionRules();
|
|
1723
|
+
uninstallModOptions();
|
|
1638
1724
|
rmSync(claudePluginRoot(), { recursive: true, force: true });
|
|
1639
1725
|
removeEmptyDirectory(dirname(claudePluginRoot()));
|
|
1640
1726
|
removeEmptyDirectory(pluginCacheRoot);
|
|
@@ -1730,6 +1816,9 @@ const main = async () => {
|
|
|
1730
1816
|
case "doctor":
|
|
1731
1817
|
doctor(parsed);
|
|
1732
1818
|
break;
|
|
1819
|
+
case "config":
|
|
1820
|
+
configModOptions(parsed.rest);
|
|
1821
|
+
break;
|
|
1733
1822
|
case "path":
|
|
1734
1823
|
printPath(parsed);
|
|
1735
1824
|
break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="20" role="img" aria-label="release: 1.0.25"><title>release: 1.0.25</title><g shape-rendering="crispEdges"><rect width="49" height="20" fill="#080d14"/><rect x="49" width="97" height="20" fill="#ff6337"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text x="255" y="140" textLength="390" transform="scale(.1)">release</text><text x="965" y="140" textLength="870" transform="scale(.1)" fill="#080D14">1.0.25</text></g></svg>
|
package/docs/hooks.md
CHANGED
|
@@ -355,7 +355,7 @@ status line read as one family:
|
|
|
355
355
|
╭─ [🔥 LIT] · lit-loop ─────────────────────────── WORK ● ─╮
|
|
356
356
|
│ ● PLAN ━━ [WORK] ━━ ○ REVIEW goal ██░░░ 2/5 │
|
|
357
357
|
│ skills [⚡ lit-plan] [⚡ litresearch] [↯ lit-docx] │
|
|
358
|
-
╰─ ctx 42% ▰▰▱▱▱ · 5h 31% · git main
|
|
358
|
+
╰─ ctx 42% ▰▰▱▱▱ · 5h 31% · 1w 12% · git main ─────────────╯
|
|
359
359
|
```
|
|
360
360
|
|
|
361
361
|
The brackets stand for the ends of filled pills: rounded caps from the icon font with `nerd`
|
|
@@ -380,7 +380,7 @@ requests are muted grey pills.
|
|
|
380
380
|
With no chip yet, the row shows the next task while work runs, or how to start (`lit <task>`)
|
|
381
381
|
while idle.
|
|
382
382
|
- The bottom edge carries the live figures from the status line: context fill, the five-hour
|
|
383
|
-
rate
|
|
383
|
+
and weekly rate windows and the Git branch (read from `HEAD`, across a linked worktree).
|
|
384
384
|
- A notice takes the chips row for a few seconds, with a toast, when a gate fires: lit-plan not
|
|
385
385
|
saved, interface probe pending or failed, motion gate, context near the auto-handoff percent,
|
|
386
386
|
auto-handoff fired. The completion moment takes it when the last task is checked, the work
|
|
@@ -397,9 +397,9 @@ chips of the current request stay bright until the main turn completes. An agent
|
|
|
397
397
|
as ended stops counting at once, and one quiet for 30 minutes (no tool call, no status change)
|
|
398
398
|
stops counting too, so a crashed or lost agent cannot hold the card at work.
|
|
399
399
|
|
|
400
|
-
The card is the terminal's width up to
|
|
400
|
+
The card is the terminal's width up to 120 columns and every row is fitted to it, with Hangul and
|
|
401
401
|
emoji counted as two cells. Nothing wraps: on a narrow terminal the Git branch goes first, then
|
|
402
|
-
the
|
|
402
|
+
the weekly and five-hour windows, the lanes, the autoloop count and the long phase labels (`P ━ W ━ R`).
|
|
403
403
|
|
|
404
404
|
The spinner gains ` · <discipline> · n/m` after its ellipsis. `/lit-evidence` opens a pane with
|
|
405
405
|
three tabs (keys `1`, `2`, `3`): the goal's criteria with their last evidence, blockers and the
|
|
@@ -408,17 +408,26 @@ newest litgoal ledger entries; the lanes; and the gates.
|
|
|
408
408
|
Motion: entrances pop in 60 ms apart with a slight overshoot (160 ms each), fills and colour
|
|
409
409
|
changes ease in and out (300 ms and 140 ms), notices slide in over 220 ms and leave over 150 ms,
|
|
410
410
|
a chip's arrival light runs 400 ms (a plain fade under `subtle`, nothing under `off`), and the
|
|
411
|
-
finish lasts at most 1.2 s.
|
|
412
|
-
|
|
411
|
+
finish lasts at most 1.2 s. While LitClaude is at work the card keeps moving so you can tell it
|
|
412
|
+
is active at a glance: the LIT pill breathes from pink to flame orange every 2.4 s, a light sweeps
|
|
413
|
+
across the active phase and this request's skill chips every 1.8 s, and a comet runs along the top rule and back along the
|
|
414
|
+
bottom one every 2.6 s. Under `subtle` only the breathing stays; under `off` nothing moves. An
|
|
415
|
+
idle card holds still. The band's `Client` element animates on Claude Code's frame clock, every
|
|
416
|
+
33 ms while a transition plays and every 66 ms during work, and stops its timer once the card is
|
|
417
|
+
idle and settled. In Claude
|
|
413
418
|
Code 2.1.292 a terminal row whose only change is a non-ASCII glyph is not repainted, so every
|
|
414
419
|
moving part changes ASCII text or colour: bar cells are coloured spaces, embers are ASCII sparks,
|
|
415
420
|
a chip's light moves over its ASCII name, and a glyph swaps only together with its words.
|
|
416
421
|
|
|
417
|
-
Options, in `/config` under LitClaude (stored in `pluginConfigs`
|
|
422
|
+
Options, in `/config` under LitClaude (stored in `pluginConfigs` in `~/.claude/settings.json`,
|
|
423
|
+
which the terminal and the desktop app's Code tab both read). The installer writes each default
|
|
424
|
+
you have not set, so the desktop app, which has no editor for them, starts with all three;
|
|
425
|
+
`litclaude-ai config` prints them and `litclaude-ai config <option> <value>` changes one.
|
|
426
|
+
Uninstall removes only the defaults it wrote that are still unchanged.
|
|
418
427
|
|
|
419
428
|
| Option | Values | Default |
|
|
420
429
|
| --- | --- | --- |
|
|
421
|
-
| `motion` | `full`, `subtle` (no overshoot, shimmer or
|
|
430
|
+
| `motion` | `full`, `subtle` (no overshoot, shimmer, embers, sweep or comet, shorter; the LIT pill still breathes during work), `off` (final frames only) | `full` |
|
|
422
431
|
| `glyphs` | `auto` (Nerd glyphs when the icon font is installed), `nerd`, or `basic` | `auto` |
|
|
423
432
|
| `band` | `always` (the whole session, idle included), `auto` (only while work is active or a skill of this request is lit), `off` | `always` |
|
|
424
433
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@litfamily/litclaude",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
4
4
|
"description": "Claude Code plugin: add lit to a prompt and Claude plans, tests, checks and reviews the work, then leaves a handoff for the next session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "litclaude",
|
|
3
3
|
"description": "Claude Code-native workflow plugin with a bounded-authority start-work lifecycle.",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.25",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "LitClaude contributors"
|
|
7
7
|
},
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"motion": {
|
|
19
19
|
"type": "string",
|
|
20
20
|
"title": "LIT band motion",
|
|
21
|
-
"description": "How the band above the prompt moves: full,
|
|
21
|
+
"description": "How the band above the prompt moves: full (during work the LIT pill breathes, a light sweeps the phase and a comet runs the border), subtle (only the breathing) or off.",
|
|
22
22
|
"default": "full"
|
|
23
23
|
},
|
|
24
24
|
"glyphs": {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// The LIT band's surface module: the `Client` element lit-mod.mjs draws above the prompt. It runs on
|
|
2
2
|
// the drawing thread with no mods API, keeps the band's timeline in its local state, and animates on
|
|
3
|
-
// the surface's own frame clock
|
|
4
|
-
//
|
|
3
|
+
// the surface's own frame clock: fast while a transition plays, slower while the ambient loop says
|
|
4
|
+
// LitClaude is at work, and not at all once the card is idle and settled, when it stays still until
|
|
5
|
+
// the hooks module hands it a new model.
|
|
5
6
|
|
|
6
7
|
import { composeBand } from "../lib/mod/frame.mjs";
|
|
7
8
|
import { TIMINGS, motionProfile } from "../lib/mod/motion.mjs";
|
|
8
9
|
import { renderBand } from "../lib/mod/render.mjs";
|
|
9
|
-
import { advanceTimeline, createTimeline, sampleTimeline, timelineBusy } from "../lib/mod/timeline.mjs";
|
|
10
|
+
import { advanceTimeline, ambientActive, createTimeline, sampleTimeline, timelineBusy } from "../lib/mod/timeline.mjs";
|
|
10
11
|
|
|
11
12
|
// The timeline lives in one holder object that the frame timer and every draw share. The timer
|
|
12
13
|
// never reads `surface.state` back, so a draw that folded in a new model is never overwritten by a
|
|
@@ -17,7 +18,7 @@ export default function LitBand(props, surface) {
|
|
|
17
18
|
const empty = () => surface.elements.Box({ children: [] });
|
|
18
19
|
if (!model) return empty();
|
|
19
20
|
|
|
20
|
-
const holder = surface.state?.live ?? { now: 0, model: null, timeline: null, stop: null };
|
|
21
|
+
const holder = surface.state?.live ?? { now: 0, model: null, timeline: null, stop: null, interval: null };
|
|
21
22
|
if (holder.timeline === null) {
|
|
22
23
|
holder.timeline = createTimeline(model, 0, profile);
|
|
23
24
|
holder.model = model;
|
|
@@ -26,19 +27,25 @@ export default function LitBand(props, surface) {
|
|
|
26
27
|
holder.model = model;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
holder.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
const interval = () => (timelineBusy(holder.timeline, holder.now, profile) ? TIMINGS.frame
|
|
31
|
+
: ambientActive(holder.timeline, profile) ? TIMINGS.ambientFrame : null);
|
|
32
|
+
const schedule = () => {
|
|
33
|
+
const wanted = interval();
|
|
34
|
+
if (wanted === holder.interval) return;
|
|
35
|
+
holder.stop?.();
|
|
36
|
+
holder.stop = null;
|
|
37
|
+
holder.interval = wanted;
|
|
38
|
+
if (wanted === null) return;
|
|
39
|
+
holder.stop = surface.every(wanted, () => {
|
|
40
|
+
holder.now += wanted;
|
|
41
|
+
schedule();
|
|
36
42
|
surface.setState({ live: holder, now: holder.now });
|
|
37
43
|
});
|
|
38
|
-
}
|
|
44
|
+
};
|
|
45
|
+
schedule();
|
|
39
46
|
if (surface.state?.live !== holder) surface.setState({ live: holder, now: holder.now });
|
|
40
47
|
|
|
41
|
-
if (!model.visible &&
|
|
48
|
+
if (!model.visible && interval() === null) return empty();
|
|
42
49
|
const frame = composeBand(model, sampleTimeline(holder.timeline, holder.now, profile), { columns: props.columns, glyphs: props.glyphs, accent: props.accent });
|
|
43
50
|
return renderBand(surface.elements, frame);
|
|
44
51
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// ╭─ [🔥 LIT] · lit-loop ─────────────────────────── WORK ● ─╮
|
|
8
8
|
// │ ● PLAN ━━ [WORK] ━━ ○ REVIEW goal ██░░░ 2/5 │
|
|
9
9
|
// │ skills [⚡ lit-plan] [⚡ litresearch] [↯ lit-docx] │
|
|
10
|
-
// ╰─ ctx 42% ▰▰▱▱▱ · 5h 31% · git main
|
|
10
|
+
// ╰─ ctx 42% ▰▰▱▱▱ · 5h 31% · 1w 12% · git main ─────────────╯
|
|
11
11
|
//
|
|
12
12
|
// Each bracket pair stands for the two end cells of a filled pill: the brand on pink, the active
|
|
13
13
|
// phase and this request's skills on the accent with dark text, skills from earlier requests on
|
|
@@ -44,7 +44,7 @@ export const PALETTE = Object.freeze({
|
|
|
44
44
|
// The status line's level colours (xterm 71, 178, 167) and its empty bar cells (238).
|
|
45
45
|
const HUD = Object.freeze({ green: "#5faf5f", yellow: "#d7af00", red: "#d75f5f", empty: "#444444" });
|
|
46
46
|
// Past this many columns the card stops growing, so a wide terminal does not stretch it thin.
|
|
47
|
-
export const CARD_MAX =
|
|
47
|
+
export const CARD_MAX = 120;
|
|
48
48
|
// Pill colours: dark text on the bright fills, the muted pill for earlier skills and +N, the brand
|
|
49
49
|
// pink of the status line's prefix, and the connectors between phases.
|
|
50
50
|
const PILL = Object.freeze({ ink: "#0b0f14", muted: "#2d333b", mutedText: "#adb5bf", brand: PALETTE.pink, rail: "#46505c" });
|
|
@@ -145,18 +145,52 @@ const fitJoined = (segments, room) => {
|
|
|
145
145
|
|
|
146
146
|
const joinSegments = (kept, joinerColor) => kept.flatMap((part, index) => (index > 0 ? [span(part.joiner, { fg: joinerColor }), ...part.spans] : part.spans));
|
|
147
147
|
|
|
148
|
+
const COMET_TAIL = 12;
|
|
149
|
+
|
|
150
|
+
// The comet's colour `k` of the way from its tail (0) to its head (1): flame orange fading in, pink
|
|
151
|
+
// through the body and white at the head, laid over the rule's own colour.
|
|
152
|
+
const cometColor = (base, k) => {
|
|
153
|
+
const hue = k > 0.8 ? lerpColor(PALETTE.pink, "#ffffff", (k - 0.8) / 0.2) : lerpColor(PALETTE.orange, PALETTE.pink, k / 0.8);
|
|
154
|
+
return lerpColor(base, hue, Math.min(1, k * 1.6));
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// The rule of an edge, `count` cells from column `start`. With a comet ({ x, dir }: its head's
|
|
158
|
+
// column and travel direction) the cells behind the head light up, brightest at the head, and the
|
|
159
|
+
// rule becomes one span per cell; without one it stays a single span.
|
|
160
|
+
const ruleSpans = (count, color, start, comet) => {
|
|
161
|
+
if (!comet) return [span("─".repeat(count), { fg: color })];
|
|
162
|
+
return Array.from({ length: count }, (_, i) => {
|
|
163
|
+
const behind = comet.dir * (comet.x - (start + i));
|
|
164
|
+
const k = behind >= 0 && behind <= COMET_TAIL ? 1 - behind / COMET_TAIL : 0;
|
|
165
|
+
return span("─", { fg: k > 0 ? cometColor(color, k) : color });
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
|
|
148
169
|
// An edge of the card: `open` and `close` are its corners, `left` its title or figures and
|
|
149
170
|
// `right` the phase indicator, with the rule filling whatever room is left.
|
|
150
|
-
const edgeRow = (open, left, right, close, width, color) => {
|
|
171
|
+
const edgeRow = (open, left, right, close, width, color, comet = null) => {
|
|
151
172
|
const tail = right.length ? [span(" "), ...right, span(` ${close}`, { fg: color })] : [span(close, { fg: color })];
|
|
152
173
|
const fill = Math.max(1, width - textWidth(open) - rowWidth(left) - 1 - rowWidth(tail));
|
|
153
|
-
|
|
174
|
+
const start = textWidth(open) + rowWidth(left) + 1;
|
|
175
|
+
return clipRow([span(open, { fg: color }), ...left, span(" "), ...ruleSpans(fill, color, start, comet), ...tail], width);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Where the ambient comet is on this card: along the top rule left to right for the first half of
|
|
179
|
+
// its loop, then back along the bottom rule, starting and ending just off the card.
|
|
180
|
+
const cometOn = (sample, edge, width) => {
|
|
181
|
+
const u = sample.ambient?.comet;
|
|
182
|
+
if (u === null || u === undefined) return null;
|
|
183
|
+
const span = width + 2 * COMET_TAIL;
|
|
184
|
+
if (edge === "top") return u < 0.5 ? { x: u * 2 * span - COMET_TAIL, dir: 1 } : null;
|
|
185
|
+
return u >= 0.5 ? { x: width + COMET_TAIL - (u - 0.5) * 2 * span, dir: -1 } : null;
|
|
154
186
|
};
|
|
155
187
|
|
|
188
|
+
// While LitClaude works the brand pill breathes from its pink toward the flame orange and back.
|
|
156
189
|
const brandSpans = (sample, glyphs) => {
|
|
157
190
|
const reveal = revealText("🔥 LIT", sample.pills[0]);
|
|
158
191
|
if (reveal === null) return [];
|
|
159
|
-
|
|
192
|
+
const fill = sample.ambient ? lerpColor(PILL.brand, PALETTE.orange, 0.85 * sample.ambient.breath) : PILL.brand;
|
|
193
|
+
return pill(litText(reveal, { fg: PILL.ink, bg: fill, bold: true }, sample.completion?.shimmer ?? null), fill, glyphs);
|
|
160
194
|
};
|
|
161
195
|
const indicatorSpans = (model, sample, glyphs, accent) => {
|
|
162
196
|
if (sample.completion) return [span(`DONE ${glyphs.done}`, { fg: PALETTE.green, bold: true })];
|
|
@@ -177,10 +211,11 @@ const topEdge = (model, sample, { width, glyphs, accent, edge }) => {
|
|
|
177
211
|
const title = (text) => (text ? [span(" · ", { fg: PALETTE.dim }), span(text, nameStyle)] : []);
|
|
178
212
|
const fixed = textWidth("╭─ ") + rowWidth(brand) + 1 + 1;
|
|
179
213
|
const withRight = fixed + rowWidth(title(name)) + 1 + rowWidth(right) + textWidth(" ─╮");
|
|
180
|
-
|
|
214
|
+
const comet = cometOn(sample, "top", width);
|
|
215
|
+
if (withRight <= width) return edgeRow("╭─ ", [...brand, ...title(name)], right, "─╮", width, edge, comet);
|
|
181
216
|
const room = width - fixed - textWidth("─╮") - textWidth(" · ");
|
|
182
217
|
const shown = room >= 4 ? truncateToWidth(name, room) : "";
|
|
183
|
-
return edgeRow("╭─ ", [...brand, ...title(shown)], [], "─╮", width, edge);
|
|
218
|
+
return edgeRow("╭─ ", [...brand, ...title(shown)], [], "─╮", width, edge, comet);
|
|
184
219
|
};
|
|
185
220
|
|
|
186
221
|
const barSpans = (fraction, sample, cells) => barCells(fraction, cells, sample.fillGlow ?? 0)
|
|
@@ -223,7 +258,8 @@ const phaseTrack = (model, sample, { glyphs, accent, rail, short }) => {
|
|
|
223
258
|
if (out.length) out.push(span(` ${glyphs.rail.repeat(rail)} `, { fg: sample.phases[PHASES[index - 1]].to === "done" ? ink.done : PILL.rail }));
|
|
224
259
|
if (state === "now") {
|
|
225
260
|
const fill = model.idle ? lerpColor(phaseColor(id, sample, ink), PILL.muted, 0.5) : phaseColor(id, sample, ink);
|
|
226
|
-
|
|
261
|
+
const sweep = model.idle ? null : sample.ambient?.sweep ?? null;
|
|
262
|
+
out.push(...pill(litText(shown.slice(0, label.length).padEnd(label.length), { fg: PILL.ink, bg: fill, bold: true }, sweep), fill, glyphs));
|
|
227
263
|
} else {
|
|
228
264
|
out.push(span(warned(id, sample) ? shown.replace(/^[●○]/u, glyphs.warn) : shown, { fg: phaseColor(id, sample, ink) }));
|
|
229
265
|
}
|
|
@@ -264,7 +300,7 @@ const CHIP_TONE = Object.freeze({
|
|
|
264
300
|
// One chip pill: the bolt and the skill name, on the accent for this request or muted for an
|
|
265
301
|
// earlier one. A chip that just arrived carries full motion's light along its name, or fills up
|
|
266
302
|
// from the muted pill under subtle; a chip changing state crossfades its colours.
|
|
267
|
-
const chipSpans = (chip, timed, glyphs, accent) => {
|
|
303
|
+
const chipSpans = (chip, timed, glyphs, accent, sweep = null) => {
|
|
268
304
|
const to = timed?.to ?? chip.state;
|
|
269
305
|
const from = timed?.from ?? to;
|
|
270
306
|
const mix = timed?.mix ?? 1;
|
|
@@ -275,7 +311,7 @@ const chipSpans = (chip, timed, glyphs, accent) => {
|
|
|
275
311
|
const bold = to === "current";
|
|
276
312
|
return pill([
|
|
277
313
|
span(`${to === "current" ? glyphs.bolt : glyphs.recentBolt} `, { fg: text, bg: fill }),
|
|
278
|
-
...litText(chip.id, { fg: text, bg: fill, bold }, timed?.shimmer),
|
|
314
|
+
...litText(chip.id, { fg: text, bg: fill, bold }, timed?.shimmer ?? (to === "current" && arrive >= 1 ? sweep : null)),
|
|
279
315
|
], fill, glyphs);
|
|
280
316
|
};
|
|
281
317
|
|
|
@@ -296,7 +332,8 @@ const chipsRow = (model, sample, { inner, glyphs, accent }) => {
|
|
|
296
332
|
const row = inner >= 30 ? [span("skills", { fg: PALETTE.dim })] : [];
|
|
297
333
|
let shown = 0;
|
|
298
334
|
for (const chip of chips) {
|
|
299
|
-
|
|
335
|
+
// While LitClaude works the light that crosses the active phase crosses this request's chips too.
|
|
336
|
+
const entry = chipSpans(chip, sample.chips?.[chip.id], glyphs, accent, model.idle ? null : sample.ambient?.sweep ?? null);
|
|
300
337
|
const gap = row.length ? [span(" ")] : [];
|
|
301
338
|
const rest = chips.length - shown - 1;
|
|
302
339
|
const reserve = rest > 0 ? 1 + rowWidth(mutedPill(`+${rest}`, glyphs)) : 0;
|
|
@@ -346,10 +383,10 @@ const completionRow = (sample, inner) => {
|
|
|
346
383
|
|
|
347
384
|
const levelColor = (percent) => (percent >= 90 ? HUD.red : percent >= 70 ? HUD.yellow : HUD.green);
|
|
348
385
|
|
|
349
|
-
// ╰─ ctx 42% ▰▰▱▱▱ · 5h 31% · git main ───╯, or a warning first when one is up. The branch goes
|
|
350
|
-
// first on a narrow card, then the
|
|
351
|
-
const bottomEdge = (model, { width, glyphs, accent, edge }) => {
|
|
352
|
-
const { context,
|
|
386
|
+
// ╰─ ctx 42% ▰▰▱▱▱ · 5h 31% · 1w 12% · git main ───╯, or a warning first when one is up. The branch goes
|
|
387
|
+
// first on a narrow card, then the weekly window, the five-hour one and the context bar.
|
|
388
|
+
const bottomEdge = (model, sample, { width, glyphs, accent, edge }) => {
|
|
389
|
+
const { context, rates, git } = model.figures;
|
|
353
390
|
const warning = model.warnings[0];
|
|
354
391
|
const segments = [];
|
|
355
392
|
if (warning) {
|
|
@@ -361,19 +398,21 @@ const bottomEdge = (model, { width, glyphs, accent, edge }) => {
|
|
|
361
398
|
const filled = Math.max(0, Math.min(5, Math.round(context / 20)));
|
|
362
399
|
segments.push(segment("ctxbar", 2, [span("▰".repeat(filled), { fg: accent }), span("▱".repeat(5 - filled), { fg: HUD.empty })], " "));
|
|
363
400
|
}
|
|
364
|
-
|
|
401
|
+
rates.forEach((rate, index) => segments.push(segment(`rate-${rate.label}`, 3 + index * 0.5, [span(`${rate.label} `, { fg: PALETTE.dim }), span(`${rate.percent}%`, { fg: levelColor(rate.percent) })], " · ")));
|
|
365
402
|
if (git) segments.push(segment("git", 4, [span(`${glyphs.branch} `, { fg: PALETTE.dim }), span(git, { fg: accent })], " · "));
|
|
366
403
|
const room = width - textWidth("╰─ ") - 1 - 1 - textWidth("─╯");
|
|
367
404
|
let kept = fitJoined(segments, room);
|
|
368
405
|
if (kept[0]?.id === "ctxbar") kept = kept.slice(1);
|
|
369
406
|
if (kept.length === 0 && warning) kept = [{ ...segments[0], spans: clipRow(segments[0].spans, room) }];
|
|
370
|
-
return edgeRow("╰─ ", joinSegments(kept, edge), [], "─╯", width, edge);
|
|
407
|
+
return edgeRow("╰─ ", joinSegments(kept, edge), [], "─╯", width, edge, cometOn(sample, "bottom", width));
|
|
371
408
|
};
|
|
372
409
|
|
|
373
|
-
|
|
410
|
+
// The border: red or amber under a warning, otherwise the accent, dimmer while idle and breathing
|
|
411
|
+
// toward full accent with the brand pill while LitClaude works.
|
|
412
|
+
const edgeColor = (model, sample, accent) => {
|
|
374
413
|
if (model.warnings.some(({ tone }) => tone === "red")) return PALETTE.red;
|
|
375
414
|
if (model.warnings.length) return PALETTE.amber;
|
|
376
|
-
return lerpColor(PALETTE.faint, accent, model.idle ? 0.35 : 0.6);
|
|
415
|
+
return lerpColor(PALETTE.faint, accent, model.idle ? 0.35 : 0.6 + 0.4 * (sample.ambient?.breath ?? 0));
|
|
377
416
|
};
|
|
378
417
|
|
|
379
418
|
/**
|
|
@@ -385,7 +424,7 @@ export const composeBand = (model, sample, { columns, glyphs: glyphMode = "basic
|
|
|
385
424
|
const glyphs = glyphSet(glyphMode);
|
|
386
425
|
const width = Math.max(12, Math.min(CARD_MAX, columns));
|
|
387
426
|
const inner = width - 4;
|
|
388
|
-
const edge = edgeColor(model, accent);
|
|
427
|
+
const edge = edgeColor(model, sample, accent);
|
|
389
428
|
const side = (row) => [span("│ ", { fg: edge }), ...row, span(" │", { fg: edge })];
|
|
390
429
|
const options = { width, inner, glyphs, accent, edge };
|
|
391
430
|
const middle = sample.completion ? completionRow(sample, inner) : sample.notice ? noticeRow(sample, inner) : chipsRow(model, sample, options);
|
|
@@ -393,7 +432,7 @@ export const composeBand = (model, sample, { columns, glyphs: glyphMode = "basic
|
|
|
393
432
|
mode: "card",
|
|
394
433
|
border: edge,
|
|
395
434
|
width,
|
|
396
|
-
rows: [topEdge(model, sample, options), side(phaseRow(model, sample, options)), side(middle), bottomEdge(model, options)],
|
|
435
|
+
rows: [topEdge(model, sample, options), side(phaseRow(model, sample, options)), side(middle), bottomEdge(model, sample, options)],
|
|
397
436
|
};
|
|
398
437
|
};
|
|
399
438
|
|
|
@@ -27,18 +27,24 @@ export const TIMINGS = Object.freeze({
|
|
|
27
27
|
completion: 1200,
|
|
28
28
|
exit: 150,
|
|
29
29
|
frame: 33,
|
|
30
|
+
breath: 2400,
|
|
31
|
+
sweep: 1800,
|
|
32
|
+
comet: 2600,
|
|
33
|
+
ambientFrame: 66,
|
|
30
34
|
});
|
|
31
35
|
|
|
32
|
-
// full: the approved motion
|
|
33
|
-
//
|
|
36
|
+
// full: the approved motion, and while LitClaude works the brand breathes, a light sweeps the active
|
|
37
|
+
// phase and a comet runs around the card's rules. subtle: no overshoot, no shimmer or embers, three
|
|
38
|
+
// quarters the time, and only the breathing while working. off: every duration is zero, so the band
|
|
39
|
+
// draws its final frame and never starts a frame timer.
|
|
34
40
|
export const motionProfile = (mode) => {
|
|
35
41
|
if (mode === "off") {
|
|
36
|
-
return { mode, duration: () => 0, enterEase: EASE.linear, noticeEase: EASE.linear, shimmer: false, embers: false };
|
|
42
|
+
return { mode, duration: () => 0, enterEase: EASE.linear, noticeEase: EASE.linear, shimmer: false, embers: false, ambient: null };
|
|
37
43
|
}
|
|
38
44
|
if (mode === "subtle") {
|
|
39
|
-
return { mode, duration: (ms) => Math.round(ms * 0.75), enterEase: EASE.outCubic, noticeEase: EASE.outCubic, shimmer: false, embers: false };
|
|
45
|
+
return { mode, duration: (ms) => Math.round(ms * 0.75), enterEase: EASE.outCubic, noticeEase: EASE.outCubic, shimmer: false, embers: false, ambient: { sweep: false, comet: false } };
|
|
40
46
|
}
|
|
41
|
-
return { mode: "full", duration: (ms) => ms, enterEase: EASE.outBack, noticeEase: EASE.outBack, shimmer: true, embers: true };
|
|
47
|
+
return { mode: "full", duration: (ms) => ms, enterEase: EASE.outBack, noticeEase: EASE.outBack, shimmer: true, embers: true, ambient: { sweep: true, comet: true } };
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
export const clamp01 = (x) => (x <= 0 ? 0 : x >= 1 ? 1 : x);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// one; the values are checked here instead, and anything unknown falls back to the default (the
|
|
4
4
|
// first choice).
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
export const MOD_CHOICES = Object.freeze({
|
|
7
7
|
motion: ["full", "subtle", "off"],
|
|
8
8
|
glyphs: ["auto", "nerd", "basic"],
|
|
9
9
|
band: ["always", "auto", "off"],
|
|
@@ -15,9 +15,9 @@ const pick = (value, choices) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export const normalizeModSettings = (options) => ({
|
|
18
|
-
motion: pick(options?.motion,
|
|
19
|
-
glyphs: pick(options?.glyphs,
|
|
20
|
-
band: pick(options?.band,
|
|
18
|
+
motion: pick(options?.motion, MOD_CHOICES.motion),
|
|
19
|
+
glyphs: pick(options?.glyphs, MOD_CHOICES.glyphs),
|
|
20
|
+
band: pick(options?.band, MOD_CHOICES.band),
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
// The glyph set one surface draws. Private-use Nerd Font glyphs only reach the terminal, which
|
|
@@ -22,9 +22,17 @@ const chipsOf = (model, now, previous = null) => Object.fromEntries((model.skill
|
|
|
22
22
|
return [chip.id, known];
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
|
+
// The ambient loop runs while the card shows LitClaude at work, so motion itself says "active";
|
|
26
|
+
// `at` is when the current run of work began, so the loop does not restart on every new model.
|
|
27
|
+
const ambientOf = (model, now, profile, previous = null) => {
|
|
28
|
+
const on = Boolean(profile.ambient) && model.visible && !model.idle;
|
|
29
|
+
return { on, at: on && previous?.on ? previous.at : now };
|
|
30
|
+
};
|
|
31
|
+
|
|
25
32
|
export const createTimeline = (model, now, profile) => {
|
|
26
33
|
const restAt = now + entryEnd(profile);
|
|
27
34
|
return {
|
|
35
|
+
ambient: ambientOf(model, now, profile),
|
|
28
36
|
enterAt: now,
|
|
29
37
|
exitAt: null,
|
|
30
38
|
fill: { from: 0, to: fractionOf(model), at: restAt, dur: profile.duration(TIMINGS.fill) },
|
|
@@ -63,9 +71,28 @@ export const advanceTimeline = (timeline, model, now, profile) => {
|
|
|
63
71
|
next.completion = model.completion ? { data: model.completion, at: now } : null;
|
|
64
72
|
}
|
|
65
73
|
next.chips = chipsOf(model, now, timeline.chips);
|
|
74
|
+
next.ambient = ambientOf(model, now, profile, timeline.ambient);
|
|
66
75
|
return next;
|
|
67
76
|
};
|
|
68
77
|
|
|
78
|
+
/** True while the ambient loop runs; the surface module then keeps a slower frame timer going. */
|
|
79
|
+
export const ambientActive = (timeline, profile) => Boolean(profile.ambient) && timeline.exitAt === null && timeline.ambient.on;
|
|
80
|
+
|
|
81
|
+
// One frame of the ambient loop: `breath` (0 to 1 and back over TIMINGS.breath), where the light
|
|
82
|
+
// stands on the active phase (`sweep`, null between passes) and how far the comet has gone around
|
|
83
|
+
// the card (`comet`, 0 to 1: the top rule left to right, then the bottom rule right to left).
|
|
84
|
+
const ambientSample = (timeline, now, profile) => {
|
|
85
|
+
if (!ambientActive(timeline, profile)) return null;
|
|
86
|
+
const elapsed = Math.max(0, now - timeline.ambient.at);
|
|
87
|
+
const breath = 0.5 - 0.5 * Math.cos((2 * Math.PI * elapsed) / TIMINGS.breath);
|
|
88
|
+
const pass = ((elapsed % TIMINGS.sweep) / TIMINGS.sweep) * 1.6 - 0.3;
|
|
89
|
+
return {
|
|
90
|
+
breath,
|
|
91
|
+
sweep: profile.ambient.sweep && pass >= -0.2 && pass <= 1.2 ? pass : null,
|
|
92
|
+
comet: profile.ambient.comet ? (elapsed % TIMINGS.comet) / TIMINGS.comet : null,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
69
96
|
export const timelineBusy = (timeline, now, profile) => {
|
|
70
97
|
const d = profile.duration;
|
|
71
98
|
if (timeline.exitAt !== null) return now < timeline.exitAt + d(TIMINGS.exit);
|
|
@@ -142,6 +169,7 @@ export const sampleTimeline = (timeline, now, profile) => {
|
|
|
142
169
|
notice,
|
|
143
170
|
completion,
|
|
144
171
|
chips,
|
|
172
|
+
ambient: ambientSample(timeline, now, profile),
|
|
145
173
|
};
|
|
146
174
|
};
|
|
147
175
|
|
|
@@ -137,14 +137,16 @@ const goalSummaryOf = (goal) => {
|
|
|
137
137
|
return { status: goal.status, passed: goal.criteria.filter(({ status }) => status === "pass").length, total: goal.criteria.length };
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
-
// The bottom edge's live figures, as the status line shows them: context fill, the five-hour
|
|
141
|
-
//
|
|
140
|
+
// The bottom edge's live figures, as the status line shows them: context fill, the five-hour and
|
|
141
|
+
// weekly windows and the Git branch.
|
|
142
142
|
const figuresOf = (usage, git) => {
|
|
143
143
|
const context = Number.isFinite(usage?.context?.percent) ? Math.round(usage.context.percent) : null;
|
|
144
144
|
const windows = Array.isArray(usage?.rateLimits) ? usage.rateLimits : [];
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
const rates = [["five_hour", "5h"], ["seven_day", "1w"]].flatMap(([kind, label]) => {
|
|
146
|
+
const window = windows.find((entry) => entry.kind === kind);
|
|
147
|
+
return window && Number.isFinite(window.percentUsed) ? [{ label, percent: Math.round(window.percentUsed) }] : [];
|
|
148
|
+
});
|
|
149
|
+
return { context, rates, git: typeof git === "string" && git ? git : null };
|
|
148
150
|
};
|
|
149
151
|
|
|
150
152
|
/**
|