@azure-id/orc 1.0.0 → 1.2.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/CHANGELOG.md +151 -0
- package/README.md +84 -34
- package/bin/cli.js +1110 -0
- package/bin/verify-contracts.js +112 -1
- package/bin/verify-package.js +568 -563
- package/bin/webui/api.js +15 -0
- package/bin/webui/app.html +210 -207
- package/bin/webui/css/panels/wait.css +123 -0
- package/bin/webui/fixtures/index.js +7 -0
- package/bin/webui/fixtures/wait.js +97 -0
- package/bin/webui/i18n/en/nav.json +21 -20
- package/bin/webui/i18n/en/wait.json +41 -0
- package/bin/webui/i18n/id/nav.json +21 -20
- package/bin/webui/i18n/id/wait.json +41 -0
- package/bin/webui/js/01-i18n.js +151 -150
- package/bin/webui/js/panels/wait.js +253 -0
- package/package.json +1 -1
- package/templates/commands/orc-wait.md +19 -0
- package/templates/hooks/orc-statusline.js +227 -1
- package/templates/skills/_shared/phases/execution.md +2 -0
- package/templates/skills/_shared/phases/preflight.md +22 -0
- package/templates/skills/_shared/return-validation.md +222 -145
- package/templates/skills/_shared/wait.md +240 -0
- package/templates/skills/orc/SKILL.md +247 -238
- package/templates/skills/orc-aftermath/SKILL.md +6 -1
- package/templates/skills/orc-analyze/SKILL.md +6 -1
- package/templates/skills/orc-boundary/SKILL.md +6 -1
- package/templates/skills/orc-brainstorm/SKILL.md +6 -1
- package/templates/skills/orc-budget/SKILL.md +6 -1
- package/templates/skills/orc-challenge/SKILL.md +6 -1
- package/templates/skills/orc-claude/SKILL.md +6 -1
- package/templates/skills/orc-diy/SKILL.md +6 -1
- package/templates/skills/orc-doc/SKILL.md +490 -481
- package/templates/skills/orc-explain/SKILL.md +5 -0
- package/templates/skills/orc-export/SKILL.md +5 -0
- package/templates/skills/orc-fast/SKILL.md +222 -215
- package/templates/skills/orc-grill/SKILL.md +6 -1
- package/templates/skills/orc-learn/SKILL.md +6 -1
- package/templates/skills/orc-mini/SKILL.md +252 -244
- package/templates/skills/orc-pact/SKILL.md +6 -1
- package/templates/skills/orc-pattern/SKILL.md +6 -1
- package/templates/skills/orc-poly/SKILL.md +6 -1
- package/templates/skills/orc-quick/SKILL.md +353 -346
- package/templates/skills/orc-retro/SKILL.md +6 -1
- package/templates/skills/orc-route/SKILL.md +6 -1
- package/templates/skills/orc-verify/SKILL.md +6 -1
- package/templates/skills/orc-wait/SKILL.md +163 -0
- package/templates/skills/orc-wiki/SKILL.md +180 -171
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* fixtures/wait.js — canned `orc usage check` / `orc wait …` responses.
|
|
4
|
+
*
|
|
5
|
+
* ONE OF EVERY STATE, including the ugly ones. You cannot design an `unknown`
|
|
6
|
+
* chip on a project whose statusline is happily writing a reading every second,
|
|
7
|
+
* and `unknown` is the state that matters most here: it is what every install
|
|
8
|
+
* on Claude Code before v2.1.80 sees, forever.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// LOW, and low on the WORST window rather than the obvious one. The 5-hour
|
|
12
|
+
// window is the one people watch; this fixture has it comfortable and the
|
|
13
|
+
// weekly one nearly gone, which is the case a panel gets wrong.
|
|
14
|
+
const usage = {
|
|
15
|
+
ok: true,
|
|
16
|
+
state: "low",
|
|
17
|
+
five_hour: {
|
|
18
|
+
window: "5h",
|
|
19
|
+
used_percentage: 62,
|
|
20
|
+
remaining_percentage: 38,
|
|
21
|
+
resets_at: new Date(Date.now() + 96 * 60000).toISOString(),
|
|
22
|
+
resets_in_minutes: 96,
|
|
23
|
+
low: false,
|
|
24
|
+
},
|
|
25
|
+
seven_day: {
|
|
26
|
+
window: "wk",
|
|
27
|
+
used_percentage: 94,
|
|
28
|
+
remaining_percentage: 6,
|
|
29
|
+
resets_at: new Date(Date.now() + 2 * 86400000).toISOString(),
|
|
30
|
+
resets_in_minutes: 2880,
|
|
31
|
+
low: true,
|
|
32
|
+
},
|
|
33
|
+
worst: "wk",
|
|
34
|
+
context: 81,
|
|
35
|
+
stop_pct: 10,
|
|
36
|
+
gate: "wait",
|
|
37
|
+
reading_age_minutes: 2,
|
|
38
|
+
note: "read BEFORE a wave; the wave then spends tokens, so this is a reading and not a promise.",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// The state a fresh install on older Claude Code sits in permanently.
|
|
42
|
+
const usageUnknown = {
|
|
43
|
+
ok: true,
|
|
44
|
+
state: "unknown",
|
|
45
|
+
reason: "no reading in the last 30 minutes",
|
|
46
|
+
five_hour: null,
|
|
47
|
+
seven_day: null,
|
|
48
|
+
context: null,
|
|
49
|
+
stop_pct: 10,
|
|
50
|
+
gate: "warn",
|
|
51
|
+
note: "unknown is not low — a run is never stopped on a missing reading. Claude Code before v2.1.80 sends no usage headers.",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const waitLanes = {
|
|
55
|
+
ok: true,
|
|
56
|
+
modes: ["safe", "soft", "hard"],
|
|
57
|
+
checkpoints: ["full", "docset", "entry", "cycle", "snapshot", "none"],
|
|
58
|
+
lanes: [
|
|
59
|
+
{ lane: "/orc", checkpoint: "full", safe_point: "wave or phase edge", modes: ["safe", "soft", "hard"], modes_differ: true, detail: "soft forces the full checkpoint before it stops; hard writes RESUME.md only and can lose an in-flight return." },
|
|
60
|
+
{ lane: "/orc-doc", checkpoint: "full", safe_point: "wave edge", modes: ["safe", "soft", "hard"], modes_differ: true, detail: "soft forces the full checkpoint before it stops; hard writes RESUME.md only and can lose an in-flight return." },
|
|
61
|
+
{ lane: "/orc-quick", checkpoint: "entry", safe_point: "after an entry closes", modes: ["safe", "soft", "hard"], modes_differ: true, detail: "soft forces the entry checkpoint before it stops; hard writes RESUME.md only and can lose an in-flight return." },
|
|
62
|
+
{ lane: "/orc-brainstorm", checkpoint: "snapshot", safe_point: "phase edge", modes: ["safe", "soft", "hard"], modes_differ: true, detail: "soft forces the snapshot checkpoint before it stops; hard writes RESUME.md only and can lose an in-flight return." },
|
|
63
|
+
// A `none` row KEEPS ITS SLOT. Filtering it out would make "this lane has
|
|
64
|
+
// nothing to checkpoint" and "this lane does not support a wait" identical.
|
|
65
|
+
{ lane: "/orc-explain", checkpoint: "none", safe_point: "read-only, seconds long", modes: ["safe", "soft", "hard"], modes_differ: false, detail: "nothing to checkpoint — one dispatch, or a read. safe, soft and hard behave identically here." },
|
|
66
|
+
{ lane: "/orc-verify", checkpoint: "none", safe_point: "single dispatch", modes: ["safe", "soft", "hard"], modes_differ: false, detail: "nothing to checkpoint — one dispatch, or a read. safe, soft and hard behave identically here." },
|
|
67
|
+
],
|
|
68
|
+
note: "A lane not in this list does not support a wait.",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Blocked AND waiting at once — the busiest the card ever gets.
|
|
72
|
+
const waitStatus = {
|
|
73
|
+
ok: true,
|
|
74
|
+
run: "rate-limit-api",
|
|
75
|
+
waiting: true,
|
|
76
|
+
mode: "soft",
|
|
77
|
+
hop: 30,
|
|
78
|
+
hops_done: 2,
|
|
79
|
+
hops_planned: 4,
|
|
80
|
+
ends_at: new Date(Date.now() + 44 * 60000).toISOString(),
|
|
81
|
+
cancel_requested: false,
|
|
82
|
+
blocked: true,
|
|
83
|
+
block_reason: "window resets in 5m, task needs 10",
|
|
84
|
+
blocked_at: new Date(Date.now() - 12 * 60000).toISOString(),
|
|
85
|
+
block_age_minutes: 12,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// No run in flight. An empty result is an ANSWER, so it still carries an object.
|
|
89
|
+
const waitStatusNone = {
|
|
90
|
+
ok: true,
|
|
91
|
+
run: null,
|
|
92
|
+
waiting: false,
|
|
93
|
+
blocked: false,
|
|
94
|
+
note: "no run is in flight",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
module.exports = { usage, usageUnknown, waitLanes, waitStatus, waitStatusNone };
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"nav.overview": "Overview",
|
|
3
|
-
"nav.settings": "Settings",
|
|
4
|
-
"nav.lanes": "Lanes",
|
|
5
|
-
"nav.runs": "Runs",
|
|
6
|
-
"nav.knowledge": "Knowledge",
|
|
7
|
-
"nav.stats": "Stats",
|
|
8
|
-
"nav.flow": "Flow",
|
|
9
|
-
"nav.crosslink": "Crosslink",
|
|
10
|
-
"nav.learn": "Learn",
|
|
11
|
-
"nav.mockrun": "Mocked Skill Use",
|
|
12
|
-
"nav.experiment": "Experiment",
|
|
13
|
-
"nav.maintenance": "Maintenance",
|
|
14
|
-
"nav.pact": "Promises",
|
|
15
|
-
"nav.boundary": "Boundary",
|
|
16
|
-
"nav.handoff": "Self-serve",
|
|
17
|
-
"nav.challenge": "Challenge",
|
|
18
|
-
"nav.extra": "Extra",
|
|
19
|
-
"nav.
|
|
20
|
-
|
|
1
|
+
{
|
|
2
|
+
"nav.overview": "Overview",
|
|
3
|
+
"nav.settings": "Settings",
|
|
4
|
+
"nav.lanes": "Lanes",
|
|
5
|
+
"nav.runs": "Runs",
|
|
6
|
+
"nav.knowledge": "Knowledge",
|
|
7
|
+
"nav.stats": "Stats",
|
|
8
|
+
"nav.flow": "Flow",
|
|
9
|
+
"nav.crosslink": "Crosslink",
|
|
10
|
+
"nav.learn": "Learn",
|
|
11
|
+
"nav.mockrun": "Mocked Skill Use",
|
|
12
|
+
"nav.experiment": "Experiment",
|
|
13
|
+
"nav.maintenance": "Maintenance",
|
|
14
|
+
"nav.pact": "Promises",
|
|
15
|
+
"nav.boundary": "Boundary",
|
|
16
|
+
"nav.handoff": "Self-serve",
|
|
17
|
+
"nav.challenge": "Challenge",
|
|
18
|
+
"nav.extra": "Extra",
|
|
19
|
+
"nav.wait": "Wait",
|
|
20
|
+
"nav.docs": "Docs"
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"wait.title": "Wait",
|
|
3
|
+
"wait.sub": "How full your usage windows are, and the wait that answers it. A wait costs no tokens - a detached command does it, and no model runs.",
|
|
4
|
+
"wait.window": "Your windows",
|
|
5
|
+
"wait.worst": "worst: {w}",
|
|
6
|
+
"wait.age": "read 1 minute ago",
|
|
7
|
+
"wait.agePlural": "read {n} minutes ago",
|
|
8
|
+
"wait.left": "{n}% left",
|
|
9
|
+
"wait.resets": "resets in {t}",
|
|
10
|
+
"wait.context": "This conversation uses {n}% of its context.",
|
|
11
|
+
"wait.contextUnknown": "Context use: not measured.",
|
|
12
|
+
"wait.contextLarge": "The context is large. After a wait of more than one hour, start a new session and run `orc resume` - it costs less than continuing here.",
|
|
13
|
+
"wait.gateOff": "usage_gate is off, so ORC never stops a run for you. Nothing here happens until you turn it on.",
|
|
14
|
+
"wait.gateOn": "usage_gate is `{gate}`. A wave needs {pct}% of a window to remain before it starts.",
|
|
15
|
+
"wait.run": "This run",
|
|
16
|
+
"wait.noRun": "No run is in flight",
|
|
17
|
+
"wait.noRunHint": "A wait belongs to a run. With no run, `/orc-wait` simply waits, and there is nothing to hand back.",
|
|
18
|
+
"wait.startWhy": "The panel cannot start a wait. A wait lives in your Claude Code session, and this panel never runs a lane.",
|
|
19
|
+
"wait.waiting": "waiting",
|
|
20
|
+
"wait.blocked": "blocked",
|
|
21
|
+
"wait.idle": "no wait",
|
|
22
|
+
"wait.hops": "Mode `{mode}` - hop {done} of {planned}, ends {ends}.",
|
|
23
|
+
"wait.zeroTokens": "A detached command does the waiting. No model runs, and no tokens are spent.",
|
|
24
|
+
"wait.cancel": "Stop waiting",
|
|
25
|
+
"wait.cancelWhy": "The wait ends at the next hop. The hand-back is already on disk, so nothing is lost either way.",
|
|
26
|
+
"wait.cancelPending": "A stop is pending. The wait ends at the next hop.",
|
|
27
|
+
"wait.blockHead": "You blocked the gate",
|
|
28
|
+
"wait.blockAge": "Set 1 minute ago.",
|
|
29
|
+
"wait.blockAgePlural": "Set {n} minutes ago.",
|
|
30
|
+
"wait.blockRisk": "ORC does not stop this run. The risk is yours: if a window empties during a wave, the wave stops in the middle. A wait you type yourself still runs.",
|
|
31
|
+
"wait.unblock": "Restore the gate",
|
|
32
|
+
"wait.unblockWhy": "ORC can stop this run again when a window is nearly full.",
|
|
33
|
+
"wait.blockWhy": "To block the gate, type this in Claude Code. A block needs a reason, and a reason must be written at the moment you decide - that record is what makes the risk yours.",
|
|
34
|
+
"wait.lanes": "Which lanes support a wait",
|
|
35
|
+
"wait.lanesWhy": "Each lane saves different work before it stops. `full` writes a checkpoint; `none` has nothing to write, because the lane is one dispatch or a read.",
|
|
36
|
+
"wait.settings": "Settings",
|
|
37
|
+
"wait.settingsWhy": "These keys are edited on the Settings page, with every other key. Two editors for one key is one editor too many.",
|
|
38
|
+
"wait.openSettings": "Open Settings",
|
|
39
|
+
"wait.done": "Done",
|
|
40
|
+
"wait.failed": "That did not work"
|
|
41
|
+
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
{
|
|
2
|
-
"nav.overview": "Ringkasan",
|
|
3
|
-
"nav.settings": "Pengaturan",
|
|
4
|
-
"nav.lanes": "Lane",
|
|
5
|
-
"nav.runs": "Run",
|
|
6
|
-
"nav.knowledge": "Pengetahuan",
|
|
7
|
-
"nav.stats": "Statistik",
|
|
8
|
-
"nav.flow": "Flow",
|
|
9
|
-
"nav.crosslink": "Crosslink",
|
|
10
|
-
"nav.learn": "Belajar",
|
|
11
|
-
"nav.mockrun": "Contoh Penggunaan Skill",
|
|
12
|
-
"nav.experiment": "Eksperimen",
|
|
13
|
-
"nav.maintenance": "Pemeliharaan",
|
|
14
|
-
"nav.pact": "Janji",
|
|
15
|
-
"nav.boundary": "Batas",
|
|
16
|
-
"nav.handoff": "Ubah Sendiri",
|
|
17
|
-
"nav.challenge": "Challenge",
|
|
18
|
-
"nav.extra": "Extra",
|
|
19
|
-
"nav.
|
|
20
|
-
|
|
1
|
+
{
|
|
2
|
+
"nav.overview": "Ringkasan",
|
|
3
|
+
"nav.settings": "Pengaturan",
|
|
4
|
+
"nav.lanes": "Lane",
|
|
5
|
+
"nav.runs": "Run",
|
|
6
|
+
"nav.knowledge": "Pengetahuan",
|
|
7
|
+
"nav.stats": "Statistik",
|
|
8
|
+
"nav.flow": "Flow",
|
|
9
|
+
"nav.crosslink": "Crosslink",
|
|
10
|
+
"nav.learn": "Belajar",
|
|
11
|
+
"nav.mockrun": "Contoh Penggunaan Skill",
|
|
12
|
+
"nav.experiment": "Eksperimen",
|
|
13
|
+
"nav.maintenance": "Pemeliharaan",
|
|
14
|
+
"nav.pact": "Janji",
|
|
15
|
+
"nav.boundary": "Batas",
|
|
16
|
+
"nav.handoff": "Ubah Sendiri",
|
|
17
|
+
"nav.challenge": "Challenge",
|
|
18
|
+
"nav.extra": "Extra",
|
|
19
|
+
"nav.wait": "Tunggu",
|
|
20
|
+
"nav.docs": "Dokumen"
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"wait.title": "Tunggu",
|
|
3
|
+
"wait.sub": "Seberapa penuh jendela pemakaian Anda, dan penantian yang menjawabnya. Menunggu tidak memakai token - sebuah perintah terpisah yang menunggu, dan tidak ada model yang berjalan.",
|
|
4
|
+
"wait.window": "Jendela Anda",
|
|
5
|
+
"wait.worst": "terburuk: {w}",
|
|
6
|
+
"wait.age": "dibaca 1 menit lalu",
|
|
7
|
+
"wait.agePlural": "dibaca {n} menit lalu",
|
|
8
|
+
"wait.left": "sisa {n}%",
|
|
9
|
+
"wait.resets": "reset dalam {t}",
|
|
10
|
+
"wait.context": "Percakapan ini memakai {n}% konteksnya.",
|
|
11
|
+
"wait.contextUnknown": "Pemakaian konteks: tidak terukur.",
|
|
12
|
+
"wait.contextLarge": "Konteksnya besar. Setelah menunggu lebih dari satu jam, mulai sesi baru dan jalankan `orc resume` - biayanya lebih kecil daripada melanjutkan di sini.",
|
|
13
|
+
"wait.gateOff": "usage_gate mati, jadi ORC tidak pernah menghentikan run untuk Anda. Tidak ada yang terjadi sampai Anda menyalakannya.",
|
|
14
|
+
"wait.gateOn": "usage_gate adalah `{gate}`. Sebuah wave perlu sisa {pct}% dari satu jendela sebelum mulai.",
|
|
15
|
+
"wait.run": "Run ini",
|
|
16
|
+
"wait.noRun": "Tidak ada run yang berjalan",
|
|
17
|
+
"wait.noRunHint": "Penantian milik sebuah run. Tanpa run, `/orc-wait` hanya menunggu, dan tidak ada yang perlu diserahkan kembali.",
|
|
18
|
+
"wait.startWhy": "Panel tidak bisa memulai penantian. Penantian hidup di sesi Claude Code Anda, dan panel ini tidak pernah menjalankan lane.",
|
|
19
|
+
"wait.waiting": "menunggu",
|
|
20
|
+
"wait.blocked": "diblokir",
|
|
21
|
+
"wait.idle": "tidak menunggu",
|
|
22
|
+
"wait.hops": "Mode `{mode}` - hop {done} dari {planned}, selesai {ends}.",
|
|
23
|
+
"wait.zeroTokens": "Sebuah perintah terpisah yang menunggu. Tidak ada model yang berjalan, dan tidak ada token yang terpakai.",
|
|
24
|
+
"wait.cancel": "Berhenti menunggu",
|
|
25
|
+
"wait.cancelWhy": "Penantian berakhir pada hop berikutnya. Serah-terima sudah ada di disk, jadi tidak ada yang hilang.",
|
|
26
|
+
"wait.cancelPending": "Permintaan berhenti tertunda. Penantian berakhir pada hop berikutnya.",
|
|
27
|
+
"wait.blockHead": "Anda memblokir gerbangnya",
|
|
28
|
+
"wait.blockAge": "Dipasang 1 menit lalu.",
|
|
29
|
+
"wait.blockAgePlural": "Dipasang {n} menit lalu.",
|
|
30
|
+
"wait.blockRisk": "ORC tidak menghentikan run ini. Risikonya milik Anda: jika sebuah jendela habis saat wave berjalan, wave berhenti di tengah. Penantian yang Anda ketik sendiri tetap berjalan.",
|
|
31
|
+
"wait.unblock": "Pulihkan gerbangnya",
|
|
32
|
+
"wait.unblockWhy": "ORC boleh menghentikan run ini lagi saat sebuah jendela hampir penuh.",
|
|
33
|
+
"wait.blockWhy": "Untuk memblokir gerbangnya, ketik ini di Claude Code. Blokir perlu alasan, dan alasan harus ditulis saat Anda memutuskan - catatan itulah yang membuat risikonya milik Anda.",
|
|
34
|
+
"wait.lanes": "Lane mana yang mendukung penantian",
|
|
35
|
+
"wait.lanesWhy": "Tiap lane menyimpan pekerjaan yang berbeda sebelum berhenti. `full` menulis checkpoint; `none` tidak punya yang perlu ditulis, karena lane itu satu dispatch atau satu bacaan.",
|
|
36
|
+
"wait.settings": "Pengaturan",
|
|
37
|
+
"wait.settingsWhy": "Kunci-kunci ini diubah di halaman Pengaturan, bersama kunci lainnya. Dua editor untuk satu kunci adalah satu editor terlalu banyak.",
|
|
38
|
+
"wait.openSettings": "Buka Pengaturan",
|
|
39
|
+
"wait.done": "Selesai",
|
|
40
|
+
"wait.failed": "Tidak berhasil"
|
|
41
|
+
}
|
package/bin/webui/js/01-i18n.js
CHANGED
|
@@ -1,150 +1,151 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/* 01-i18n.js — orc ui client
|
|
3
|
-
LANGS, loadLang, setLang, t, applyStaticText, cycleLang.
|
|
4
|
-
|
|
5
|
-
Keys are written out IN FULL, never assembled from a fragment. The v0.48.1
|
|
6
|
-
file split is by key PREFIX only; `docs.ship.confirm` is still spelled
|
|
7
|
-
`docs.ship.confirm` at every call site.
|
|
8
|
-
|
|
9
|
-
Loaded by app.html in the order its numeric prefix names. Classic
|
|
10
|
-
script, no import/export: an ES module import carries no query string,
|
|
11
|
-
and every static request here needs the per-launch session token. */
|
|
12
|
-
|
|
13
|
-
/* ------------------------------------------------------------------- i18n -- */
|
|
14
|
-
/*
|
|
15
|
-
THE SCOPE RULE, and it is deliberately narrow: only the panel's OWN prose is
|
|
16
|
-
translated. Everything that arrives from `bin/cli.js --json` stays exactly as
|
|
17
|
-
the CLI wrote it — config keys and their descriptions, values, agent names,
|
|
18
|
-
model ids, file paths, commands, `orc doctor` messages, command output. Those
|
|
19
|
-
are identifiers, not sentences: a translated config key is a key that does not
|
|
20
|
-
exist, and a translated command is a command you cannot type. So `t()` is
|
|
21
|
-
never applied to a value read out of a payload, and neither string table
|
|
22
|
-
contains a single ORC key name.
|
|
23
|
-
|
|
24
|
-
The tables are plain JSON served as static assets. Adding a language is a
|
|
25
|
-
FOLDER in i18n/ plus one row in LANGS — no build step, no library, no
|
|
26
|
-
dependency, and no server change (serve.js walks i18n/ at boot).
|
|
27
|
-
*/
|
|
28
|
-
const LANGS = [
|
|
29
|
-
{ code: "en", label: "English" },
|
|
30
|
-
{ code: "id", label: "Bahasa Indonesia" },
|
|
31
|
-
];
|
|
32
|
-
const LANG_KEY = "orc-ui-lang";
|
|
33
|
-
|
|
34
|
-
// v0.48.1 — one file per namespace instead of one 53 KB table, so changing the
|
|
35
|
-
// Docs panel's wording means opening i18n/en/docs.json rather than paging
|
|
36
|
-
// through every string in the panel.
|
|
37
|
-
//
|
|
38
|
-
// A plain array, and deliberately NOT an index file to fetch: an index would be
|
|
39
|
-
// one more round trip and one more thing to forget to update. A namespace owns
|
|
40
|
-
// one or more key PREFIXES (`stats` owns `stats.*` and `cost.*`), which the
|
|
41
|
-
// test suite asserts — a key whose prefix belongs to no namespace would load
|
|
42
|
-
// nowhere and render as a raw dotted string.
|
|
43
|
-
const NAMESPACES = [
|
|
44
|
-
"common",
|
|
45
|
-
"nav",
|
|
46
|
-
"banner",
|
|
47
|
-
"overview",
|
|
48
|
-
"settings",
|
|
49
|
-
"lanes",
|
|
50
|
-
"runs",
|
|
51
|
-
"knowledge",
|
|
52
|
-
"stats",
|
|
53
|
-
"flow",
|
|
54
|
-
"crosslink",
|
|
55
|
-
"learn",
|
|
56
|
-
"mockrun",
|
|
57
|
-
"maintenance",
|
|
58
|
-
"pact",
|
|
59
|
-
"boundary",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (s === undefined) s = key;
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
/* 01-i18n.js — orc ui client
|
|
3
|
+
LANGS, loadLang, setLang, t, applyStaticText, cycleLang.
|
|
4
|
+
|
|
5
|
+
Keys are written out IN FULL, never assembled from a fragment. The v0.48.1
|
|
6
|
+
file split is by key PREFIX only; `docs.ship.confirm` is still spelled
|
|
7
|
+
`docs.ship.confirm` at every call site.
|
|
8
|
+
|
|
9
|
+
Loaded by app.html in the order its numeric prefix names. Classic
|
|
10
|
+
script, no import/export: an ES module import carries no query string,
|
|
11
|
+
and every static request here needs the per-launch session token. */
|
|
12
|
+
|
|
13
|
+
/* ------------------------------------------------------------------- i18n -- */
|
|
14
|
+
/*
|
|
15
|
+
THE SCOPE RULE, and it is deliberately narrow: only the panel's OWN prose is
|
|
16
|
+
translated. Everything that arrives from `bin/cli.js --json` stays exactly as
|
|
17
|
+
the CLI wrote it — config keys and their descriptions, values, agent names,
|
|
18
|
+
model ids, file paths, commands, `orc doctor` messages, command output. Those
|
|
19
|
+
are identifiers, not sentences: a translated config key is a key that does not
|
|
20
|
+
exist, and a translated command is a command you cannot type. So `t()` is
|
|
21
|
+
never applied to a value read out of a payload, and neither string table
|
|
22
|
+
contains a single ORC key name.
|
|
23
|
+
|
|
24
|
+
The tables are plain JSON served as static assets. Adding a language is a
|
|
25
|
+
FOLDER in i18n/ plus one row in LANGS — no build step, no library, no
|
|
26
|
+
dependency, and no server change (serve.js walks i18n/ at boot).
|
|
27
|
+
*/
|
|
28
|
+
const LANGS = [
|
|
29
|
+
{ code: "en", label: "English" },
|
|
30
|
+
{ code: "id", label: "Bahasa Indonesia" },
|
|
31
|
+
];
|
|
32
|
+
const LANG_KEY = "orc-ui-lang";
|
|
33
|
+
|
|
34
|
+
// v0.48.1 — one file per namespace instead of one 53 KB table, so changing the
|
|
35
|
+
// Docs panel's wording means opening i18n/en/docs.json rather than paging
|
|
36
|
+
// through every string in the panel.
|
|
37
|
+
//
|
|
38
|
+
// A plain array, and deliberately NOT an index file to fetch: an index would be
|
|
39
|
+
// one more round trip and one more thing to forget to update. A namespace owns
|
|
40
|
+
// one or more key PREFIXES (`stats` owns `stats.*` and `cost.*`), which the
|
|
41
|
+
// test suite asserts — a key whose prefix belongs to no namespace would load
|
|
42
|
+
// nowhere and render as a raw dotted string.
|
|
43
|
+
const NAMESPACES = [
|
|
44
|
+
"common",
|
|
45
|
+
"nav",
|
|
46
|
+
"banner",
|
|
47
|
+
"overview",
|
|
48
|
+
"settings",
|
|
49
|
+
"lanes",
|
|
50
|
+
"runs",
|
|
51
|
+
"knowledge",
|
|
52
|
+
"stats",
|
|
53
|
+
"flow",
|
|
54
|
+
"crosslink",
|
|
55
|
+
"learn",
|
|
56
|
+
"mockrun",
|
|
57
|
+
"maintenance",
|
|
58
|
+
"pact",
|
|
59
|
+
"boundary",
|
|
60
|
+
"wait",
|
|
61
|
+
"handoff",
|
|
62
|
+
"challenge",
|
|
63
|
+
"docs",
|
|
64
|
+
"extra",
|
|
65
|
+
"experiment",
|
|
66
|
+
"tour",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
let lang = "en";
|
|
70
|
+
// English is loaded first and kept as the FALLBACK table: a key the other
|
|
71
|
+
// language has not translated yet renders in English rather than as a raw
|
|
72
|
+
// dotted key. A half-finished translation degrades to mixed prose, never to
|
|
73
|
+
// debug output on somebody's screen.
|
|
74
|
+
let DICT = {};
|
|
75
|
+
let DICT_EN = {};
|
|
76
|
+
|
|
77
|
+
// The namespaces are fetched in parallel and merged into ONE flat table, so
|
|
78
|
+
// `t()` is unchanged and no call site knows the split happened.
|
|
79
|
+
//
|
|
80
|
+
// A namespace that fails to load falls back to ENGLISH for its keys rather than
|
|
81
|
+
// rejecting the whole language: a network hiccup on one file should cost that
|
|
82
|
+
// file's prose, not the entire panel. English itself has no fallback below it,
|
|
83
|
+
// so a failure there rejects and boot() lands on raw keys — ugly, never blank.
|
|
84
|
+
function loadLang(code) {
|
|
85
|
+
return Promise.all(
|
|
86
|
+
NAMESPACES.map((ns) =>
|
|
87
|
+
fetch(`/i18n/${code}/${ns}.json`, { headers: { "x-orc-token": TOKEN }, cache: "no-store" })
|
|
88
|
+
.then((r) => {
|
|
89
|
+
if (!r.ok) throw new Error(`i18n ${code}/${ns} ${r.status}`);
|
|
90
|
+
return r.json();
|
|
91
|
+
})
|
|
92
|
+
.catch((e) => {
|
|
93
|
+
if (code === "en") throw e;
|
|
94
|
+
return null;
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
).then((parts) => Object.assign({}, ...parts.filter(Boolean)));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A missing or broken table must never blank the UI. Every failure path here
|
|
101
|
+
// lands on English, so the worst case is untranslated text — never empty text.
|
|
102
|
+
async function setLang(code, opts) {
|
|
103
|
+
const rerender = !opts || opts.rerender !== false;
|
|
104
|
+
const want = LANGS.some((l) => l.code === code) ? code : "en";
|
|
105
|
+
try {
|
|
106
|
+
DICT = want === "en" ? DICT_EN : await loadLang(want);
|
|
107
|
+
lang = want;
|
|
108
|
+
} catch (_) {
|
|
109
|
+
DICT = DICT_EN;
|
|
110
|
+
lang = "en";
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
localStorage.setItem(LANG_KEY, lang);
|
|
114
|
+
} catch (_) {}
|
|
115
|
+
document.documentElement.setAttribute("lang", lang);
|
|
116
|
+
applyStaticText();
|
|
117
|
+
if (rerender) route();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function t(key, vars) {
|
|
121
|
+
let s = DICT[key];
|
|
122
|
+
if (s === undefined) s = DICT_EN[key];
|
|
123
|
+
if (s === undefined) s = key;
|
|
124
|
+
if (vars) for (const k of Object.keys(vars)) s = s.split("{" + k + "}").join(String(vars[k]));
|
|
125
|
+
return s;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// English has one plural form and Indonesian has none, so count-aware lookup is
|
|
129
|
+
// a second key rather than a plural-rules engine: `n === 1` picks the singular.
|
|
130
|
+
const tn = (n, key, vars) => t(n === 1 ? key : key + "Plural", Object.assign({ n }, vars || {}));
|
|
131
|
+
|
|
132
|
+
// The parts of the shell that are markup rather than a render function. Called
|
|
133
|
+
// on every language change; panels re-render themselves through `route()`.
|
|
134
|
+
function applyStaticText() {
|
|
135
|
+
for (const node of document.querySelectorAll("[data-i18n]")) node.textContent = t(node.dataset.i18n);
|
|
136
|
+
const tt = $("#theme-toggle");
|
|
137
|
+
if (tt) tt.textContent = document.documentElement.getAttribute("data-theme") === "light" ? t("rail.dark") : t("rail.light");
|
|
138
|
+
const hint = $("#shortcut-hint");
|
|
139
|
+
if (hint) hint.title = t("rail.shortcuts");
|
|
140
|
+
const label = $("#lang-label");
|
|
141
|
+
if (label) label.textContent = (LANGS.find((l) => l.code === lang) || LANGS[0]).label;
|
|
142
|
+
const btn = $("#lang-toggle");
|
|
143
|
+
if (btn) btn.title = t("lang.title");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Two languages, so the control is a TOGGLE rather than a menu: one click, no
|
|
147
|
+
// dropdown to open. A third language would make this a <select>; two do not.
|
|
148
|
+
function cycleLang() {
|
|
149
|
+
const i = LANGS.findIndex((l) => l.code === lang);
|
|
150
|
+
setLang(LANGS[(i + 1) % LANGS.length].code);
|
|
151
|
+
}
|