jaitus 0.3.7 → 0.3.8
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/package.json +1 -1
- package/src/agent/loop.js +8 -1
- package/src/agent/permissions.js +3 -1
- package/src/agent/rules.js +27 -3
- package/src/agent/tools.js +39 -2
- package/src/api.js +3 -1
- package/src/errors.js +2 -1
- package/src/index.js +142 -36
- package/src/skills/builtin/documents/SKILL.md +91 -0
- package/src/skills/builtin/frontend-design/SKILL.md +102 -0
- package/src/skills.js +6 -1
package/package.json
CHANGED
package/src/agent/loop.js
CHANGED
|
@@ -106,6 +106,9 @@ How to work:
|
|
|
106
106
|
- Run the project's own tests or build after a change when one exists, and report the real result — including when it fails.
|
|
107
107
|
- Use run_command for anything the shell does better: git, tests, package managers, linters.
|
|
108
108
|
- Never invent a file path, a function name, or a test result. If you are unsure, look.
|
|
109
|
+
- Every turn re-sends the whole conversation, so make independent reads, searches and
|
|
110
|
+
listings together in ONE turn rather than one per turn. Read the part of a large file you
|
|
111
|
+
need (offset/limit) rather than all of it.
|
|
109
112
|
- For current news or other web facts, report only details grounded in web_search or
|
|
110
113
|
web_fetch output. Include the supplied source URLs; never fill a missing headline or
|
|
111
114
|
date from memory.
|
|
@@ -241,7 +244,11 @@ export function repairToolPairs(messages) {
|
|
|
241
244
|
// at least TRIM_AT_LEAST at a time, because each clearing breaks the prompt cache once
|
|
242
245
|
// — clearing a little on every step would cost more than it saves. Small results (a
|
|
243
246
|
// "file written", an exit code) are kept: they carry meaning and cost nothing.
|
|
244
|
-
|
|
247
|
+
// 100k was the documented default for 200k-window models. Here the window is up to 1M, so
|
|
248
|
+
// that ceiling was rarely near: production requests averaged 53k tokens and two thirds of
|
|
249
|
+
// all input tokens came from requests over 60k. At 40k a clearing (one cache break, ~17
|
|
250
|
+
// credits on glm-5.3-flash) pays for itself within about ten later requests.
|
|
251
|
+
export const TRIM_AT = 40_000; // estimated tokens of conversation
|
|
245
252
|
export const KEEP_RECENT = 6; // latest tool results always kept whole
|
|
246
253
|
export const TRIM_AT_LEAST = 12_000; // estimated tokens a clearing must free
|
|
247
254
|
const TRIM_MIN_CHARS = 1_500; // results shorter than this are never cleared
|
package/src/agent/permissions.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { c, out, askKey, glyph } from '../ui.js';
|
|
19
|
-
import { decide, ruleForCall, broaderRule, DEFAULT_DENY, subjectOf } from './rules.js';
|
|
19
|
+
import { decide, ruleForCall, broaderRule, DEFAULT_DENY, subjectOf, wideAllows } from './rules.js';
|
|
20
20
|
|
|
21
21
|
export class PermissionDenied extends Error {}
|
|
22
22
|
|
|
@@ -44,6 +44,8 @@ export function createPermissions({
|
|
|
44
44
|
const api = {
|
|
45
45
|
get mode() { return current; },
|
|
46
46
|
get rules() { return { allow: [...savedAllow], deny: [...savedDeny], session: [...sessionAllow] }; },
|
|
47
|
+
/** Saved rules that take a whole tool out of the mode's hands (see wideAllows). */
|
|
48
|
+
get wide() { return wideAllows(savedAllow); },
|
|
47
49
|
|
|
48
50
|
/** Add a rule. `persist` writes it to settings via onRulesChange. */
|
|
49
51
|
addRule(rule, { persist = false, kind = 'allow' } = {}) {
|
package/src/agent/rules.js
CHANGED
|
@@ -206,15 +206,39 @@ export function ruleForCall(toolName, input, cwd) {
|
|
|
206
206
|
return `${toolName}(${relative && !relative.startsWith('..') ? relative : subject})`;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* A broader rule, offered beside the exact one: the file's directory. Never the whole
|
|
211
|
+
* tool. "Always allow every run command" used to be the second option on every command
|
|
212
|
+
* prompt; one click on it saved a rule that approves every command in that folder in
|
|
213
|
+
* every mode, forever — after which Ask, Auto-edit and Bypass all behaved the same and
|
|
214
|
+
* nothing on screen said why. A tool-wide rule can still be written on purpose with
|
|
215
|
+
* `/permissions allow run_command`; it is just never one click away.
|
|
216
|
+
*/
|
|
210
217
|
export function broaderRule(toolName, input, cwd) {
|
|
211
218
|
const subject = subjectOf(toolName, input);
|
|
212
219
|
if (['write_file', 'edit_file', 'read_file', 'read_image', 'list_files'].includes(toolName) && subject) {
|
|
213
220
|
const relative = cwd ? normalise(path.relative(cwd, path.resolve(cwd, subject))) : subject;
|
|
214
221
|
const dir = relative.includes('/') ? relative.slice(0, relative.lastIndexOf('/')) : '';
|
|
215
|
-
|
|
222
|
+
if (dir && !dir.startsWith('..')) return `${toolName}(${dir}/**)`;
|
|
223
|
+
}
|
|
224
|
+
return ruleForCall(toolName, input, cwd);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Saved allow rules that approve a whole tool — `run_command`, `edit_file(**)` — for the
|
|
229
|
+
* tools a mode is about. While one exists, the mode no longer decides that tool, so the
|
|
230
|
+
* mode's label is only half the truth; callers say so beside it.
|
|
231
|
+
*/
|
|
232
|
+
export function wideAllows(allow = []) {
|
|
233
|
+
const GATED = ['run_command', 'background_run', 'write_file', 'edit_file'];
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const rule of allow) {
|
|
236
|
+
const parsed = typeof rule === 'string' ? parseRule(rule) : rule;
|
|
237
|
+
if (!parsed || !GATED.includes(parsed.tool)) continue;
|
|
238
|
+
const p = parsed.pattern;
|
|
239
|
+
if (p === null || p === '' || p === '*' || p === '**' || p === '**/*') out.push(parsed.source);
|
|
216
240
|
}
|
|
217
|
-
return
|
|
241
|
+
return out;
|
|
218
242
|
}
|
|
219
243
|
|
|
220
244
|
/**
|
package/src/agent/tools.js
CHANGED
|
@@ -26,6 +26,18 @@ const MAX_READ_LINES = 2_000;
|
|
|
26
26
|
|
|
27
27
|
export class ToolError extends Error {}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Keep the start AND the end. For a test run or a build the end is where the failures and
|
|
31
|
+
* the summary are; cutting from the front kept the noise and threw away the answer.
|
|
32
|
+
*/
|
|
33
|
+
function truncateMiddle(text, limit) {
|
|
34
|
+
const s = String(text ?? '');
|
|
35
|
+
if (s.length <= limit) return s;
|
|
36
|
+
const head = Math.floor(limit / 3);
|
|
37
|
+
const tail = limit - head;
|
|
38
|
+
return `${s.slice(0, head)}\n\n[… ${s.length - limit} characters cut from the middle …]\n\n${s.slice(-tail)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
function truncate(text, limit = MAX_OUTPUT) {
|
|
30
42
|
const s = String(text ?? '');
|
|
31
43
|
if (s.length <= limit) return s;
|
|
@@ -560,8 +572,10 @@ export const TOOLS = [
|
|
|
560
572
|
// the joined text cut the exit code off the end of any long output, so the
|
|
561
573
|
// model could not tell whether the command had worked.
|
|
562
574
|
const parts = [];
|
|
563
|
-
|
|
564
|
-
|
|
575
|
+
// 30k in all (the common agent limit for shell output): every later request
|
|
576
|
+
// re-sends it until it is cleared.
|
|
577
|
+
const stdout = truncateMiddle(out.text().trim(), 22_000);
|
|
578
|
+
const stderr = truncateMiddle(err.text().trim(), 8_000);
|
|
565
579
|
if (stdout) parts.push(stdout);
|
|
566
580
|
if (stderr) parts.push(`stderr:\n${stderr}`);
|
|
567
581
|
if (note) parts.push(note);
|
|
@@ -832,6 +846,29 @@ export const TOOLS = [
|
|
|
832
846
|
return `Stopped ${input.id}.`;
|
|
833
847
|
},
|
|
834
848
|
},
|
|
849
|
+
|
|
850
|
+
{
|
|
851
|
+
// The model loads a skill's instructions itself when a task matches one. Only each
|
|
852
|
+
// skill's one-line description rides along with every request; the body (a few
|
|
853
|
+
// thousand tokens) is paid for only in conversations that need it.
|
|
854
|
+
name: 'load_skill',
|
|
855
|
+
description: 'Load the full instructions of a skill listed under "Skills" in the system prompt. Call it BEFORE starting a task that matches a skill\'s description, then follow what it says.',
|
|
856
|
+
needsPermission: false,
|
|
857
|
+
input_schema: {
|
|
858
|
+
type: 'object',
|
|
859
|
+
properties: { name: { type: 'string', description: 'The skill name, e.g. frontend-design.' } },
|
|
860
|
+
required: ['name'],
|
|
861
|
+
},
|
|
862
|
+
summarise: (i) => `skill ${i.name}`,
|
|
863
|
+
async run(input, { cwd }) {
|
|
864
|
+
const { discoverSkills } = await import('../skills.js');
|
|
865
|
+
const skills = discoverSkills(cwd || process.cwd());
|
|
866
|
+
const name = String(input.name || '').replace(/^\//, '').trim();
|
|
867
|
+
const skill = skills[name];
|
|
868
|
+
if (!skill) throw new ToolError(`No skill named ${name}. Available: ${Object.keys(skills).join(', ') || 'none'}.`);
|
|
869
|
+
return `Skill: ${skill.name}\n\n${skill.content}`;
|
|
870
|
+
},
|
|
871
|
+
},
|
|
835
872
|
];
|
|
836
873
|
|
|
837
874
|
/** The live task list todo_write maintains; the session renders it in the panel. */
|
package/src/api.js
CHANGED
|
@@ -18,7 +18,9 @@ export function createClient() {
|
|
|
18
18
|
const { baseUrl } = endpoints();
|
|
19
19
|
if (!signedIn()) throw new NotSignedIn('Not signed in. Run `jaitus login` first.');
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// x-jaitus-features: what this build understands, so the gateway can use newer error
|
|
22
|
+
// codes (a used-up usage window) without an older CLI reading them as "went wrong".
|
|
23
|
+
const base = { 'Content-Type': 'application/json', 'user-agent': 'jaitus-cli', 'x-jaitus-features': 'usage-window' };
|
|
22
24
|
const headers = (token) => ({ ...base, authorization: `Bearer ${token}` });
|
|
23
25
|
|
|
24
26
|
return {
|
package/src/errors.js
CHANGED
|
@@ -18,7 +18,8 @@ const CATALOG = {
|
|
|
18
18
|
rate_limited: { title: 'Slow down', hint: 'Wait a moment, then retry.' },
|
|
19
19
|
context_too_long: { title: 'Conversation too long', hint: 'Run /compact, or start a new session.' },
|
|
20
20
|
request_too_large: { title: 'Request too large', hint: 'Usually images: remove some, or run /compact to drop old ones. Images over 3.5 MB are shrunk automatically.' },
|
|
21
|
-
daily_limit_reached: { title: '
|
|
21
|
+
daily_limit_reached: { title: 'Usage limit reached', hint: 'It refills automatically. /usage shows where you are.' },
|
|
22
|
+
window_limit_reached: { title: 'This usage window is used up', hint: 'It refills when the window ends — the rest of your month is still there. /usage shows when.' },
|
|
22
23
|
token_expired: { title: 'Signed out', hint: 'Type /login to sign in again (or run `jaitus login`).' },
|
|
23
24
|
refresh_race: { title: 'Sign-in was being renewed', hint: 'Try again.' },
|
|
24
25
|
content_blocked: { title: 'Request refused', hint: 'Rephrase the request and try again.' },
|
package/src/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
endpoints, version, HOME, HISTORY,
|
|
18
18
|
} from './config.js';
|
|
19
19
|
import { runAgent, repairToolPairs } from './agent/loop.js';
|
|
20
|
-
import { createPermissions } from './agent/permissions.js';
|
|
20
|
+
import { createPermissions, MODES as PERMISSION_MODES } from './agent/permissions.js';
|
|
21
21
|
import { DEFAULT_DENY } from './agent/rules.js';
|
|
22
22
|
import { forgetIgnoreCache, imageBlock, IMAGE_MIME } from './agent/tools.js';
|
|
23
23
|
import { shrinkImageFile } from './tui/clipboard.js';
|
|
@@ -372,6 +372,11 @@ async function loadContextBudget(client) {
|
|
|
372
372
|
// Compact at this fraction of the budget. Low enough that the summarisation request
|
|
373
373
|
// itself still fits comfortably, high enough that a normal session never sees it.
|
|
374
374
|
const AUTO_COMPACT_AT = 0.78;
|
|
375
|
+
// The budget is what the gateway advertises — the providers' limit, or the admin's
|
|
376
|
+
// Context cap when it is lower (Admin → Settings → Plans & credits). That cap is the ONE
|
|
377
|
+
// control for how big a session gets before it is summarised: a second ceiling here
|
|
378
|
+
// would silently override whatever the admin set.
|
|
379
|
+
const compactThreshold = () => CONTEXT_BUDGET * AUTO_COMPACT_AT;
|
|
375
380
|
|
|
376
381
|
// An expanded @mention is capped: pulling a 4 MB lockfile into the prompt because
|
|
377
382
|
// someone typed `@package-lock.json` would blow the context it is meant to save.
|
|
@@ -421,8 +426,20 @@ async function summarise(client, model, messages, focus) {
|
|
|
421
426
|
messages: [
|
|
422
427
|
{
|
|
423
428
|
role: 'user',
|
|
424
|
-
|
|
425
|
-
|
|
429
|
+
// Sections a successor needs to carry on without re-asking or re-reading — the
|
|
430
|
+
// same structure agent tools use for their own compaction. Losing the user's
|
|
431
|
+
// exact instructions or where the work stopped is what makes a compacted session
|
|
432
|
+
// feel worse; everything else can be re-read from disk.
|
|
433
|
+
content: 'Summarize this conversation for a successor agent that will continue the work with no other memory of it. '
|
|
434
|
+
+ 'Use these headings, and be specific (paths, names, numbers) rather than general:\n'
|
|
435
|
+
+ '1. Request — what the user asked for, with their exact words for any instruction, constraint or preference.\n'
|
|
436
|
+
+ '2. Corrections — anything the user rejected, corrected or asked to do differently, verbatim where short.\n'
|
|
437
|
+
+ '3. Files — every file read or changed: path, what changed or what matters in it.\n'
|
|
438
|
+
+ '4. Commands and results — what was run and what it showed (test/build results, errors and how each was fixed).\n'
|
|
439
|
+
+ '5. Decisions — what was chosen and why.\n'
|
|
440
|
+
+ '6. Current state — exactly what was being done when this summary was made, and what is unfinished.\n'
|
|
441
|
+
+ '7. Next step — the single next action, if the work was mid-way.\n'
|
|
442
|
+
+ 'Omit headings that have nothing. No preamble.'
|
|
426
443
|
+ (focus ? ` Focus on: ${focus}.` : '')
|
|
427
444
|
+ `\n\n<conversation>\n${transcriptForSummary(messages)}\n</conversation>`,
|
|
428
445
|
},
|
|
@@ -502,6 +519,37 @@ function projectRules(cwd) {
|
|
|
502
519
|
return { allow: Array.isArray(saved.allow) ? saved.allow : [], deny: Array.isArray(saved.deny) ? saved.deny : [] };
|
|
503
520
|
}
|
|
504
521
|
|
|
522
|
+
/** The bypass confirmation, wherever bypass is switched on from. */
|
|
523
|
+
const BYPASS_CHOICES = [
|
|
524
|
+
{ label: 'Yes, for this session', value: 'y' },
|
|
525
|
+
{ label: 'Yes, and start every new session in bypass', value: 'always' },
|
|
526
|
+
{ label: 'No, stay put', value: 'n' },
|
|
527
|
+
];
|
|
528
|
+
const BYPASS_WARNING = [
|
|
529
|
+
'Writes and shell commands will run with NO permission prompts.',
|
|
530
|
+
'Use only in a trusted workspace you can afford to rebuild. Deny rules still apply.',
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
/** The mode every new session starts in; null goes back to "the last one used". */
|
|
534
|
+
function saveDefaultMode(mode) {
|
|
535
|
+
const settings = loadSettings();
|
|
536
|
+
if (mode) settings.defaultMode = mode;
|
|
537
|
+
else delete settings.defaultMode;
|
|
538
|
+
saveSettings(settings);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* What to say beside a mode when a saved rule already approves a whole tool here —
|
|
543
|
+
* otherwise "Ask" is on screen while every command runs unasked, and the modes look
|
|
544
|
+
* like they do nothing.
|
|
545
|
+
*/
|
|
546
|
+
function wideRuleNote(permissions, mode) {
|
|
547
|
+
if (mode === 'plan' || mode === 'bypassPermissions') return '';
|
|
548
|
+
const wide = permissions?.wide || [];
|
|
549
|
+
if (!wide.length) return '';
|
|
550
|
+
return `Saved rule ${wide.join(', ')} skips the permission question in every mode here — /permissions forget ${wide[0]} to let the mode decide.`;
|
|
551
|
+
}
|
|
552
|
+
|
|
505
553
|
/** The key rules are saved under: the existing spelling when there is one. */
|
|
506
554
|
function rulesKey(cwd) {
|
|
507
555
|
return Object.keys(loadSettings().permissionRules || {}).find((k) => sameDir(k, cwd)) || path.resolve(cwd);
|
|
@@ -547,8 +595,11 @@ async function oneShot(task, { flags }) {
|
|
|
547
595
|
messages.push({ role: 'user', content: task });
|
|
548
596
|
|
|
549
597
|
const explicitPlan = flags.permissionModeExplicit && flags.permissionMode === 'plan';
|
|
598
|
+
// The saved default mode applies here too — "every session" includes `jaitus -p`.
|
|
599
|
+
const savedDefault = loadSettings().defaultMode;
|
|
600
|
+
const oneShotMode = !flags.permissionModeExplicit && PERMISSION_MODES.includes(savedDefault) ? savedDefault : flags.permissionMode;
|
|
550
601
|
const permissions = createPermissions({
|
|
551
|
-
autoApprove: flags.yes && !explicitPlan, mode:
|
|
602
|
+
autoApprove: flags.yes && !explicitPlan, mode: oneShotMode,
|
|
552
603
|
interactive: false, ...projectRules(flags.cwd),
|
|
553
604
|
});
|
|
554
605
|
|
|
@@ -558,7 +609,7 @@ async function oneShot(task, { flags }) {
|
|
|
558
609
|
...(flags.maxTurns ? { maxIterations: flags.maxTurns } : {}),
|
|
559
610
|
silent: true,
|
|
560
611
|
systemOverride: flags.systemPrompt || null,
|
|
561
|
-
extraSystem: flags.appendSystemPrompt ? `\n\nAdditional instructions:\n${flags.appendSystemPrompt}` : ''
|
|
612
|
+
extraSystem: `${skillCatalog(discoverSkills(flags.cwd))}${flags.appendSystemPrompt ? `\n\nAdditional instructions:\n${flags.appendSystemPrompt}` : ''}`,
|
|
562
613
|
onText: (text) => {
|
|
563
614
|
answer += text;
|
|
564
615
|
if (format === 'stream-json') out(JSON.stringify({ type: 'content_block_delta', delta: { text } }));
|
|
@@ -613,7 +664,7 @@ const SLASH = {
|
|
|
613
664
|
'/tasks': { summary: 'review tool activity from this session' },
|
|
614
665
|
'/image <path>': { summary: 'attach an image (png/jpg/gif/webp) the model will see with your next message' },
|
|
615
666
|
'/memory [text]': { summary: 'show or append to JAITUS.md (# does this inline)' },
|
|
616
|
-
'/permissions [mode|rules]': { summary: 'permission mode, and the allow/deny rules for this project' },
|
|
667
|
+
'/permissions [mode|rules|default]': { summary: 'permission mode, the mode new sessions start in, and the allow/deny rules for this project' },
|
|
617
668
|
'/skills': { summary: 'list project workflows' },
|
|
618
669
|
'/sessions': { summary: 'list saved sessions in this directory' },
|
|
619
670
|
'/session': { summary: 'show this session id', hidden: true }, // the id is already in /status
|
|
@@ -778,11 +829,21 @@ async function session({ flags }) {
|
|
|
778
829
|
// flag used to be overridden by it, so after one Shift+Tab `--permission-mode plan`
|
|
779
830
|
// started a session that edited files without asking. Plan (often set by /ask or
|
|
780
831
|
// /plan, not chosen) and bypass never carry into a new session on their own.
|
|
781
|
-
|
|
782
|
-
|
|
832
|
+
// A saved default mode (`/permissions default <mode>`, or "Yes, every session" on the
|
|
833
|
+
// bypass prompt) is a choice made once, on purpose, and it wins over the last mode:
|
|
834
|
+
// turning bypass on in every new chat was the thing people had to repeat.
|
|
835
|
+
const startSettings = loadSettings();
|
|
836
|
+
const defaultMode = PERMISSION_MODES.includes(startSettings.defaultMode) ? startSettings.defaultMode : null;
|
|
837
|
+
const lastMode = startSettings.lastMode;
|
|
838
|
+
if (!flags.yes && !flags.permissionModeExplicit) {
|
|
839
|
+
if (defaultMode) permissions.setMode(defaultMode);
|
|
840
|
+
else if (['default', 'acceptEdits'].includes(lastMode)) permissions.setMode(lastMode);
|
|
841
|
+
}
|
|
842
|
+
const bypassIsDefault = defaultMode === 'bypassPermissions' && !flags.permissionModeExplicit;
|
|
783
843
|
// Starting straight into bypass gets one explicit confirmation (§11) when a
|
|
784
|
-
// human is present; headless runs just get the warning on stderr.
|
|
785
|
-
|
|
844
|
+
// human is present; headless runs just get the warning on stderr. A saved bypass
|
|
845
|
+
// default was confirmed when it was saved and is not asked again.
|
|
846
|
+
if (permissions.mode === 'bypassPermissions' && !flags.yes && !bypassIsDefault) {
|
|
786
847
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
787
848
|
out(`\n ${c.red(c.bold('● BYPASS'))} ${c.red('requested — writes and shell commands would run with no permission checks.')}`);
|
|
788
849
|
const answer = await askKey(` ${c.dim(`Enable bypass mode for this session? [y/N] ${glyph('›', '>')}`)} `, ['y', 'n', '\r', ' ']);
|
|
@@ -873,17 +934,15 @@ async function session({ flags }) {
|
|
|
873
934
|
const order = ['default', 'acceptEdits', 'plan', 'bypassPermissions'];
|
|
874
935
|
const next = order[(order.indexOf(permissions.mode) + 1) % order.length];
|
|
875
936
|
if (next === 'bypassPermissions') {
|
|
876
|
-
const ans = await tui.askDialog('Enable bypass mode?',
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
'Writes and shell commands will run with NO permission prompts.',
|
|
881
|
-
'Use only in a trusted workspace you can afford to rebuild.',
|
|
882
|
-
]);
|
|
883
|
-
if (dialogValue(ans) !== 'y') return permissions.mode;
|
|
937
|
+
const ans = await tui.askDialog('Enable bypass mode?', BYPASS_CHOICES, BYPASS_WARNING);
|
|
938
|
+
const picked = dialogValue(ans);
|
|
939
|
+
if (picked !== 'y' && picked !== 'always') return permissions.mode;
|
|
940
|
+
if (picked === 'always') saveDefaultMode('bypassPermissions');
|
|
884
941
|
}
|
|
885
942
|
const m = permissions.cycleMode();
|
|
886
943
|
refreshStatusLine('mode');
|
|
944
|
+
const note = wideRuleNote(permissions, m);
|
|
945
|
+
if (note) tui.flash?.(note, 'warn', 4500);
|
|
887
946
|
return m;
|
|
888
947
|
},
|
|
889
948
|
};
|
|
@@ -1109,7 +1168,7 @@ async function session({ flags }) {
|
|
|
1109
1168
|
const autoCompactIfNeeded = async () => {
|
|
1110
1169
|
if (loadSettings().autoCompact === false) return;
|
|
1111
1170
|
const used = estimateTokens(messages);
|
|
1112
|
-
if (used <
|
|
1171
|
+
if (used < compactThreshold()) return;
|
|
1113
1172
|
if (messages.length < 4) return;
|
|
1114
1173
|
|
|
1115
1174
|
tui.setStatus?.(`Compacting — ${format(used)} of ${format(CONTEXT_BUDGET)} context used`);
|
|
@@ -1426,6 +1485,15 @@ async function session({ flags }) {
|
|
|
1426
1485
|
showContext();
|
|
1427
1486
|
tui.setSpent?.(spent);
|
|
1428
1487
|
refreshStatusLine('start');
|
|
1488
|
+
// Why the session is in the mode it is in, when that is not obvious from the badge.
|
|
1489
|
+
const startNotes = [
|
|
1490
|
+
bypassIsDefault && permissions.mode === 'bypassPermissions' ? 'Bypass is your default for new sessions — /permissions default ask turns that off.' : '',
|
|
1491
|
+
wideRuleNote(permissions, permissions.mode),
|
|
1492
|
+
].filter(Boolean);
|
|
1493
|
+
// The welcome screen hides transcript notices until the first message, so the status
|
|
1494
|
+
// line carries them at start; the notice keeps the record in the transcript.
|
|
1495
|
+
for (const note of startNotes) { if (tui.notice) tui.notice(note, 'warn'); else tui.print(c.yellow(` ${note}`)); }
|
|
1496
|
+
if (startNotes.length) tui.flash?.(startNotes.join(' · '), 'warn', 9000);
|
|
1429
1497
|
|
|
1430
1498
|
/** Esc+Esc: pick a checkpoint, restore the conversation and the files. */
|
|
1431
1499
|
const doRewind = async () => {
|
|
@@ -1891,17 +1959,15 @@ async function slashCommand(verb, args, ctx) {
|
|
|
1891
1959
|
const applyMode = async (next) => {
|
|
1892
1960
|
const real = next === 'auto' ? 'bypassPermissions' : next;
|
|
1893
1961
|
if ((real === 'bypassPermissions') && ctx.tui && ctx.permissions.mode !== 'bypassPermissions') {
|
|
1894
|
-
const ans = await ctx.tui.askDialog('Enable bypass mode?',
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
'Writes and shell commands will run with NO permission prompts.',
|
|
1899
|
-
'Use only in a trusted workspace you can afford to rebuild.',
|
|
1900
|
-
], { title: 'Bypass mode', hideComment: true });
|
|
1901
|
-
if (dialogValue(ans) !== 'y') { info(`Staying in ${c.bold(ctx.permissions.mode)}.`); return; }
|
|
1962
|
+
const ans = await ctx.tui.askDialog('Enable bypass mode?', BYPASS_CHOICES, BYPASS_WARNING, { title: 'Bypass mode', hideComment: true });
|
|
1963
|
+
const picked = dialogValue(ans);
|
|
1964
|
+
if (picked !== 'y' && picked !== 'always') { info(`Staying in ${c.bold(ctx.permissions.mode)}.`); return; }
|
|
1965
|
+
if (picked === 'always') { saveDefaultMode('bypassPermissions'); ok('Bypass is now the default for every new session — /permissions default ask undoes it.'); }
|
|
1902
1966
|
}
|
|
1903
1967
|
ctx.permissions.setMode(real);
|
|
1904
1968
|
ok(`Permission mode: ${c.bold(next)}`);
|
|
1969
|
+
const note = wideRuleNote(ctx.permissions, real);
|
|
1970
|
+
if (note) warn(note);
|
|
1905
1971
|
if (real === 'bypassPermissions') warn('The agent will not ask before writing files or running commands.');
|
|
1906
1972
|
if (next === 'plan') info('The agent can read and think, but nothing runs until you switch out of plan mode.');
|
|
1907
1973
|
};
|
|
@@ -1910,6 +1976,27 @@ async function slashCommand(verb, args, ctx) {
|
|
|
1910
1976
|
// shows what has accumulated, which matters because "always allow" writes to
|
|
1911
1977
|
// disk and a list nobody can read is a list nobody can revoke.
|
|
1912
1978
|
const next = args[0];
|
|
1979
|
+
// ── the mode every new session starts in ──
|
|
1980
|
+
if (next === 'default' && args.length > 1 || next === 'startup') {
|
|
1981
|
+
const wanted = normaliseMode(args[1]) || (/^(off|none|last|unset)$/i.test(args[1] || '') ? 'off' : null);
|
|
1982
|
+
if (!wanted) {
|
|
1983
|
+
const current = loadSettings().defaultMode;
|
|
1984
|
+
info(`New sessions start in ${c.bold(current || 'the last mode used (ask or auto-edit)')}.`);
|
|
1985
|
+
info(`${c.dim('Change it with')} ${c.bold('/permissions default bypass')}${c.dim(', ask, acceptEdits, plan — or off.')}`);
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
if (wanted === 'bypassPermissions' && ctx.tui && loadSettings().defaultMode !== 'bypassPermissions') {
|
|
1989
|
+
const ans = await ctx.tui.askDialog('Start every new session in bypass?', [
|
|
1990
|
+
{ label: 'Yes, every session', value: 'y' },
|
|
1991
|
+
{ label: 'No', value: 'n' },
|
|
1992
|
+
], BYPASS_WARNING, { title: 'Bypass mode', hideComment: true });
|
|
1993
|
+
if (dialogValue(ans) !== 'y') { info('Unchanged.'); return null; }
|
|
1994
|
+
}
|
|
1995
|
+
saveDefaultMode(wanted === 'off' ? null : wanted);
|
|
1996
|
+
ok(wanted === 'off' ? 'New sessions start in the last mode used (ask or auto-edit).' : `New sessions start in ${c.bold(wanted)}.`);
|
|
1997
|
+
if (wanted !== 'off' && ctx.permissions.mode !== wanted) info(`This session stays in ${c.bold(ctx.permissions.mode)} — Shift+Tab or /permissions ${wanted} to switch now.`);
|
|
1998
|
+
return null;
|
|
1999
|
+
}
|
|
1913
2000
|
if (next === 'rules' || next === 'allow' || next === 'deny' || next === 'forget') {
|
|
1914
2001
|
const { parseRule } = await import('./agent/rules.js');
|
|
1915
2002
|
if (next === 'rules') {
|
|
@@ -1942,8 +2029,10 @@ async function slashCommand(verb, args, ctx) {
|
|
|
1942
2029
|
}
|
|
1943
2030
|
|
|
1944
2031
|
if (next) {
|
|
1945
|
-
|
|
1946
|
-
|
|
2032
|
+
// `bypass`, `ask`, `auto-edit` are what the badge says; they are modes too.
|
|
2033
|
+
const named = normaliseMode(next);
|
|
2034
|
+
if (!named) { warn(`Unknown mode: ${next}. Try ask, acceptEdits, plan, bypass — or ${c.bold('/permissions rules')}.`); return null; }
|
|
2035
|
+
await applyMode(named);
|
|
1947
2036
|
return null;
|
|
1948
2037
|
}
|
|
1949
2038
|
if (!ctx.tui) {
|
|
@@ -2601,7 +2690,12 @@ function planRows(u) {
|
|
|
2601
2690
|
if (st?.monthlyCredits) {
|
|
2602
2691
|
const resets = st.periodResetsAt ? ` · resets ${new Date(st.periodResetsAt).toLocaleDateString()}` : '';
|
|
2603
2692
|
rows.push(['This period', `${cr(st.periodCredits)} of ${cr(st.monthlyCredits)} credits${resets}`]);
|
|
2604
|
-
if (st.
|
|
2693
|
+
if (st.windowCredits) {
|
|
2694
|
+
const refill = st.windowResetsAt
|
|
2695
|
+
? `refills at ${new Date(st.windowResetsAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
|
|
2696
|
+
: 'opens with your next request';
|
|
2697
|
+
rows.push([`This ${st.windowHours || 5}h window`, `${cr(st.windowUsed)} of ${cr(st.windowCredits)} credits · ${refill}`]);
|
|
2698
|
+
}
|
|
2605
2699
|
if (st.periodTokens) rows.push(['Behind them', `${fmtTokens(st.periodTokens)} tokens of input and replies`]);
|
|
2606
2700
|
}
|
|
2607
2701
|
if (u.planExpiresAt) rows.push(['Renews by', new Date(u.planExpiresAt).toLocaleDateString()]);
|
|
@@ -2755,9 +2849,10 @@ const commands = {
|
|
|
2755
2849
|
'jaitus config theme <name> dark, light, ocean, daltonized, auto',
|
|
2756
2850
|
'jaitus config <key> --unset back to the default',
|
|
2757
2851
|
'jaitus config copyOnSelect <on|off> auto-copy mouse selection',
|
|
2852
|
+
'jaitus config defaultMode bypass start every session in bypass (ask, acceptEdits, plan; --unset = last mode used)',
|
|
2758
2853
|
],
|
|
2759
2854
|
detail: [
|
|
2760
|
-
'Settable: model, baseUrl, siteUrl, theme, copyOnSelect, scrollSpeed, wheelAccel, autoScroll, animations, autoCompact, trimToolResults, mouse, statusLine,',
|
|
2855
|
+
'Settable: model, baseUrl, siteUrl, theme, defaultMode, copyOnSelect, scrollSpeed, wheelAccel, autoScroll, animations, autoCompact, trimToolResults, mouse, statusLine,',
|
|
2761
2856
|
'braveSearchKey, tavilyKey, serperKey. Stored in ~/.jaitus/settings.json.',
|
|
2762
2857
|
'',
|
|
2763
2858
|
'A search API key is the single biggest improvement available to web_search: the free',
|
|
@@ -2781,6 +2876,7 @@ const commands = {
|
|
|
2781
2876
|
website: e.siteUrl,
|
|
2782
2877
|
model: settings.model || null,
|
|
2783
2878
|
theme: settings.theme || 'auto',
|
|
2879
|
+
defaultMode: settings.defaultMode || null,
|
|
2784
2880
|
statusLine: settings.statusLine || null,
|
|
2785
2881
|
copyOnSelect: settings.copyOnSelect !== false,
|
|
2786
2882
|
scrollSpeed: settings.scrollSpeed ?? 1,
|
|
@@ -2800,13 +2896,14 @@ const commands = {
|
|
|
2800
2896
|
['Website', data.website],
|
|
2801
2897
|
['Model', data.model || 'whatever the gateway lists first'],
|
|
2802
2898
|
['Theme', `${data.theme} (${[...THEME_NAMES, 'auto'].join(', ')})`],
|
|
2899
|
+
['Start mode', data.defaultMode || 'the last mode used (ask or auto-edit)'],
|
|
2803
2900
|
['Copy on select', data.copyOnSelect ? 'on (drag selects, auto-copies)' : 'off (Ctrl+Shift+C copies)'],
|
|
2804
2901
|
['Scroll speed', String(data.scrollSpeed)],
|
|
2805
2902
|
['Wheel accel', data.wheelAccel ? 'on' : 'off'],
|
|
2806
2903
|
['Auto-scroll', data.autoScroll ? 'on' : 'off'],
|
|
2807
2904
|
['Animations', data.animations ? 'on' : 'off'],
|
|
2808
|
-
['Auto-compact', data.autoCompact ? 'on (summarises
|
|
2809
|
-
['Trim old tool output', data.trimToolResults ? 'on (clears old outputs past ~
|
|
2905
|
+
['Auto-compact', data.autoCompact ? 'on (summarises at 78% of the context size the service allows)' : 'off'],
|
|
2906
|
+
['Trim old tool output', data.trimToolResults ? 'on (clears old outputs past ~40k tokens, keeps the last 6)' : 'off'],
|
|
2810
2907
|
['Mouse', data.mouse ? 'on (wheel scrolls, clicks act, drag or double-click copies)' : 'off (the terminal selects text itself)'],
|
|
2811
2908
|
['Search API', data.searchApi ? `${data.searchApi} (set)` : 'none — using free engines + npm/GitHub/MDN/Stack Overflow'],
|
|
2812
2909
|
['Status line', data.statusLine || '(unset — set a command to render one)'],
|
|
@@ -2814,13 +2911,13 @@ const commands = {
|
|
|
2814
2911
|
]));
|
|
2815
2912
|
return 0;
|
|
2816
2913
|
}
|
|
2817
|
-
if (!['model', 'baseUrl', 'siteUrl', 'theme', 'statusLine', ...ONOFF, ...SECRET, 'scrollSpeed'].includes(key)) {
|
|
2818
|
-
fail(`Unknown setting: ${key}. Try model, baseUrl, siteUrl, theme, statusLine, ${ONOFF.join(', ')}, ${SECRET.join(', ')} or scrollSpeed.`);
|
|
2914
|
+
if (!['model', 'baseUrl', 'siteUrl', 'theme', 'defaultMode', 'statusLine', ...ONOFF, ...SECRET, 'scrollSpeed'].includes(key)) {
|
|
2915
|
+
fail(`Unknown setting: ${key}. Try model, baseUrl, siteUrl, theme, defaultMode, statusLine, ${ONOFF.join(', ')}, ${SECRET.join(', ')} or scrollSpeed.`);
|
|
2819
2916
|
return 1;
|
|
2820
2917
|
}
|
|
2821
2918
|
// Clearing a value (there was no way: an empty value just printed the current one).
|
|
2822
2919
|
if (unset) {
|
|
2823
|
-
if (!['model', 'baseUrl', 'siteUrl', 'statusLine', 'theme', 'scrollSpeed', ...SECRET].includes(key)) { fail(`${key} is on/off — set it with on or off.`); return 1; }
|
|
2920
|
+
if (!['model', 'baseUrl', 'siteUrl', 'statusLine', 'theme', 'defaultMode', 'scrollSpeed', ...SECRET].includes(key)) { fail(`${key} is on/off — set it with on or off.`); return 1; }
|
|
2824
2921
|
delete settings[key];
|
|
2825
2922
|
saveSettings(settings);
|
|
2826
2923
|
ok(`${key} cleared — back to the default.`);
|
|
@@ -2836,6 +2933,15 @@ const commands = {
|
|
|
2836
2933
|
if (key === 'scrollSpeed') { out(String(settings.scrollSpeed ?? 1)); return 0; }
|
|
2837
2934
|
out(String(settings[key] ?? '')); return 0;
|
|
2838
2935
|
}
|
|
2936
|
+
if (key === 'defaultMode') {
|
|
2937
|
+
const mode = normaliseMode(value);
|
|
2938
|
+
if (!mode) { fail('Use ask, acceptEdits, plan or bypass (or --unset for the last mode used).'); return 1; }
|
|
2939
|
+
settings.defaultMode = mode;
|
|
2940
|
+
saveSettings(settings);
|
|
2941
|
+
ok(`defaultMode = ${mode} — every new session starts in it`);
|
|
2942
|
+
if (mode === 'bypassPermissions') warn('Writes and shell commands will run with no permission prompts in every session. Deny rules still apply.');
|
|
2943
|
+
return 0;
|
|
2944
|
+
}
|
|
2839
2945
|
if (key === 'theme') {
|
|
2840
2946
|
// The themes that exist (ocean was refused; plain was accepted and did nothing).
|
|
2841
2947
|
const names = [...THEME_NAMES, 'auto'];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Load BEFORE making a PDF or any printable/shareable document — report, proposal, invoice, resume/CV, letter, certificate, brochure, one-pager, handout, slide deck. Produces typeset, professional documents instead of plain text dumps.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Professional documents and PDFs
|
|
6
|
+
|
|
7
|
+
The reliable route is: **write semantic HTML with print CSS, then print it to PDF with a
|
|
8
|
+
headless browser.** It gives real typography, exact page control and is easy to revise.
|
|
9
|
+
Use a PDF library (reportlab, pdfkit, jsPDF) only if the project already uses one.
|
|
10
|
+
|
|
11
|
+
## 1. Plan the document
|
|
12
|
+
|
|
13
|
+
- What is it, who reads it, and what must they do after reading? Put that first.
|
|
14
|
+
- Pick a document direction that fits: corporate report (clean sans, blue-black ink, one
|
|
15
|
+
accent, data tables) · editorial/proposal (serif body, generous margins, pull quotes) ·
|
|
16
|
+
modern minimal (one sans, lots of white, strong headings) · formal letter/certificate
|
|
17
|
+
(centred serif, restrained ornament) · resume (dense but calm, two-column, no graphics
|
|
18
|
+
that ATS software cannot read).
|
|
19
|
+
- Outline the sections before writing; every page should have one clear purpose.
|
|
20
|
+
|
|
21
|
+
## 2. Page setup (print CSS)
|
|
22
|
+
|
|
23
|
+
```css
|
|
24
|
+
@page { size: A4; margin: 20mm 18mm 22mm; } /* or: size: Letter; */
|
|
25
|
+
@page :first { margin-top: 0; } /* full-bleed cover if wanted */
|
|
26
|
+
html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
27
|
+
body { font: 10.5pt/1.55 "Source Serif 4", Georgia, serif; color: #1b1d22; }
|
|
28
|
+
h1, h2, h3 { font-family: "Inter Tight", "Helvetica Neue", Arial, sans-serif; line-height: 1.15; break-after: avoid; }
|
|
29
|
+
h2 { break-before: auto; margin: 18pt 0 6pt; }
|
|
30
|
+
table, figure, blockquote, .card { break-inside: avoid; }
|
|
31
|
+
p { orphans: 3; widows: 3; }
|
|
32
|
+
.page-break { break-before: page; }
|
|
33
|
+
a { color: inherit; text-decoration: none; border-bottom: 0.5pt solid currentColor; }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- Use pt/mm for print, never viewport units. Body 10–11.5pt, headings on a scale
|
|
37
|
+
(e.g. 10.5 · 13 · 16 · 22 · 32pt). Line length ~65–80 characters; wide pages use two
|
|
38
|
+
columns (`columns: 2; column-gap: 8mm;`) or a wide margin column for notes.
|
|
39
|
+
- Colour: dark ink on white, ONE accent colour for headings rules, key numbers and table
|
|
40
|
+
headers, tinted greys for secondary text. Keep it printable in greyscale.
|
|
41
|
+
- Page numbers / running footer: put a footer element on each page section, or when the
|
|
42
|
+
browser supports it use `@page { @bottom-right { content: counter(page) " / " counter(pages); } }`.
|
|
43
|
+
|
|
44
|
+
## 3. Typesetting details that make it look professional
|
|
45
|
+
|
|
46
|
+
- A proper cover or title block: title, subtitle, author/organisation, date — aligned to a
|
|
47
|
+
grid, not just a big centred heading.
|
|
48
|
+
- Consistent vertical rhythm: spacing in multiples of the body line height.
|
|
49
|
+
- Tables: left-align text, right-align numbers with tabular figures
|
|
50
|
+
(`font-variant-numeric: tabular-nums`), light horizontal rules only, a bold or tinted
|
|
51
|
+
header row, units in the header, totals row emphasised. No full grid of borders.
|
|
52
|
+
- Numbers: thousands separators, consistent decimals, currency symbols (₹ 1,49,900 for
|
|
53
|
+
Indian format when the audience is Indian).
|
|
54
|
+
- Callouts/key figures as a small highlighted block, not colourful boxes everywhere.
|
|
55
|
+
- Charts: inline SVG (or Chart.js rendered before printing) with direct labels, no 3D, no
|
|
56
|
+
heavy gridlines, the accent colour for the series that matters and grey for the rest.
|
|
57
|
+
- Real content — no lorem ipsum, no "[Your Name]" left in unless it is a template.
|
|
58
|
+
- Invoices: seller/buyer blocks, invoice number, dates, line items table, tax lines, total,
|
|
59
|
+
payment details and terms — every legally expected field for the country.
|
|
60
|
+
- Resumes: name large, contact on one line, sections in a clear order, consistent date
|
|
61
|
+
alignment on the right, verbs and numbers in bullets, one page unless senior.
|
|
62
|
+
|
|
63
|
+
## 4. Render to PDF
|
|
64
|
+
|
|
65
|
+
Save the HTML (e.g. `report.html`) and print it with whichever browser is installed:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
# Windows (Edge ships with Windows)
|
|
69
|
+
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --headless --disable-gpu --no-pdf-header-footer --print-to-pdf="report.pdf" "file:///C:/path/to/report.html"
|
|
70
|
+
# Chrome (any OS)
|
|
71
|
+
chrome --headless --disable-gpu --no-pdf-header-footer --print-to-pdf=report.pdf file:///abs/path/report.html
|
|
72
|
+
# macOS Chrome
|
|
73
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless --no-pdf-header-footer --print-to-pdf=report.pdf file:///abs/path/report.html
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If the project has Node and Playwright or Puppeteer, `page.pdf({ format: 'A4',
|
|
77
|
+
printBackground: true, preferCSSPageSize: true })` works the same. Web fonts must finish
|
|
78
|
+
loading before printing — with a headless browser add `--virtual-time-budget=5000`, or
|
|
79
|
+
embed the fonts locally.
|
|
80
|
+
|
|
81
|
+
## 5. Check before handing over
|
|
82
|
+
|
|
83
|
+
1. The PDF exists and opens; report its path and page count.
|
|
84
|
+
2. No heading stranded at the bottom of a page, no table row split, no blank last page.
|
|
85
|
+
3. Margins are even, nothing clipped at the edges, backgrounds printed.
|
|
86
|
+
4. Hierarchy is obvious at a glance; one accent colour; consistent spacing.
|
|
87
|
+
5. All placeholder text replaced; numbers, dates and names are correct.
|
|
88
|
+
|
|
89
|
+
For slide decks: one idea per slide, 16:9 (`@page { size: 297mm 167mm; margin: 0 }` with
|
|
90
|
+
one `section` per slide), a big headline that states the point, at most ~30 words of body,
|
|
91
|
+
visual consistency across slides, and slide numbers.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Load BEFORE building or restyling anything people look at — a website, landing page, web app screen, dashboard, component, HTML email, or any HTML/CSS/React/Vue/Svelte UI. Makes it look designed and professional instead of generic.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Designing interfaces that look made by a designer
|
|
6
|
+
|
|
7
|
+
The default look of generated UI is instantly recognisable and reads as cheap: a blue or
|
|
8
|
+
purple gradient hero, Inter or system font everywhere, three identical rounded cards with
|
|
9
|
+
emoji icons, grey text on white, soft shadows on everything, centred everything. Avoid all
|
|
10
|
+
of it. Every screen must look like a deliberate choice by someone with taste.
|
|
11
|
+
|
|
12
|
+
## 1. Decide the direction before writing code
|
|
13
|
+
|
|
14
|
+
Write these down (in your head or a comment) first — they drive every later decision:
|
|
15
|
+
|
|
16
|
+
- **Who it is for and what it must make them feel** (trust, speed, calm, delight, authority).
|
|
17
|
+
- **One visual direction**, named concretely. Pick what suits the subject, e.g.:
|
|
18
|
+
editorial (serif display, generous whitespace, rules and columns) · Swiss/International
|
|
19
|
+
(strict grid, one sans, big type, flat colour) · warm product (off-white paper, soft ink,
|
|
20
|
+
one saturated accent) · dark technical (near-black, luminous accent, mono details) ·
|
|
21
|
+
brutalist (raw borders, heavy type, no radius) · luxury (restrained, large margins, thin
|
|
22
|
+
serif, muted palette) · playful (bold colour blocks, chunky type, big radius).
|
|
23
|
+
- **The one memorable thing** — an oversized headline, an unusual colour, a striking
|
|
24
|
+
layout, a detail. A design with nothing memorable is the generic one.
|
|
25
|
+
|
|
26
|
+
If the project already has a design system, tokens, a brand colour or a component library,
|
|
27
|
+
**follow it** — consistency beats novelty. Read the existing CSS/theme files first.
|
|
28
|
+
|
|
29
|
+
## 2. Tokens first
|
|
30
|
+
|
|
31
|
+
Define design tokens as CSS custom properties (or the theme file of the framework in use)
|
|
32
|
+
before any component, and use only them afterwards:
|
|
33
|
+
|
|
34
|
+
- **Colour:** a background, a surface, 2–3 text shades, ONE accent, borders, and semantic
|
|
35
|
+
success/warning/danger. Neutrals should be tinted toward the accent's hue (warm or cool
|
|
36
|
+
greys), never flat `#808080`. Avoid pure `#000` on pure `#fff` for large areas — use ink
|
|
37
|
+
like `#16181d` on paper like `#faf8f5`. Accent used sparingly: buttons, links, one
|
|
38
|
+
highlight. Check contrast: body text ≥ 4.5:1, large text ≥ 3:1.
|
|
39
|
+
- **Type:** two families at most — a characterful display face and a readable text face
|
|
40
|
+
(or one family with strong weight contrast). Good free pairs (Google Fonts):
|
|
41
|
+
Fraunces + Inter Tight · Instrument Serif + Geist · Space Grotesk + IBM Plex Sans ·
|
|
42
|
+
DM Serif Display + DM Sans · Playfair Display + Source Sans 3 · JetBrains Mono for code.
|
|
43
|
+
A modular scale (e.g. 1.25): 12 · 14 · 16 · 20 · 25 · 31 · 39 · 49 · 61. Headlines tight
|
|
44
|
+
(line-height 1.05–1.15, letter-spacing −0.01 to −0.03em); body 1.5–1.65; measure
|
|
45
|
+
60–75 characters. Use weight and size for hierarchy, not colour alone.
|
|
46
|
+
- **Space:** a 4 or 8px scale (4 8 12 16 24 32 48 64 96 128). Be generous — cramped is the
|
|
47
|
+
most common amateur tell. Sections breathe with 96–160px vertical padding on desktop.
|
|
48
|
+
- **Radius and elevation:** choose one radius personality (0, 4–6, or 12–16) and keep it.
|
|
49
|
+
Prefer borders and background shifts over shadows; when shadows are used, make them
|
|
50
|
+
subtle and layered (`0 1px 2px rgb(0 0 0 / .06), 0 8px 24px rgb(0 0 0 / .06)`).
|
|
51
|
+
|
|
52
|
+
## 3. Layout
|
|
53
|
+
|
|
54
|
+
- Build on a real grid (12 columns, or a clear 2–3 column system) with a max content width
|
|
55
|
+
(1120–1280px) and consistent gutters.
|
|
56
|
+
- Break symmetry on purpose: asymmetric hero (text left, visual right, offset), a wide
|
|
57
|
+
feature next to narrow ones, a pull quote, a full-bleed band. Not every section centred.
|
|
58
|
+
- Establish hierarchy in every section: one thing clearly first, then second, then detail.
|
|
59
|
+
- Align everything to the grid; mismatched left edges look broken.
|
|
60
|
+
- Mobile first-class: stack sensibly, keep 16–20px side padding, tap targets ≥ 44px, no
|
|
61
|
+
horizontal scroll. Test at 375px and 1440px widths mentally or in a browser.
|
|
62
|
+
|
|
63
|
+
## 4. Components and detail
|
|
64
|
+
|
|
65
|
+
- **Buttons:** one primary style (solid accent), one secondary (outline or ghost); clear
|
|
66
|
+
hover, focus-visible (a visible ring), active and disabled states. Label with verbs.
|
|
67
|
+
- **Icons:** one consistent SVG set (Lucide, Heroicons, Phosphor, Tabler) at one stroke
|
|
68
|
+
weight — never emoji as icons.
|
|
69
|
+
- **Imagery:** real content or tasteful placeholders — CSS/SVG shapes, a gradient mesh used
|
|
70
|
+
subtly, product mock frames. Never broken image links or stock-photo clichés.
|
|
71
|
+
- **Copy:** specific and concrete; no "Lorem ipsum", no "Welcome to our website", no
|
|
72
|
+
"Revolutionize your workflow". Real numbers and nouns.
|
|
73
|
+
- **States:** empty, loading (skeletons that match the layout), error (says what happened
|
|
74
|
+
and what to do), success. Forms: labels above inputs, helpful inline errors.
|
|
75
|
+
- **Motion:** small and purposeful — 150–250ms ease-out for hover/press, a staggered fade-up
|
|
76
|
+
on first view at most. Respect `prefers-reduced-motion`.
|
|
77
|
+
- **Dark mode** if the product warrants it: redefine the tokens, don't invert colours; dark
|
|
78
|
+
surfaces are dark grey (`#111318`), not black, with slightly desaturated accents.
|
|
79
|
+
|
|
80
|
+
## 5. Things that make it look generic — don't
|
|
81
|
+
|
|
82
|
+
- Purple/blue gradient backgrounds, gradient text on everything, glowing blobs by default.
|
|
83
|
+
- Three equal cards with an emoji, a bold title and two lines of grey text as the main idea.
|
|
84
|
+
- Every section centred with the same padding and the same card component.
|
|
85
|
+
- Default Tailwind blue (`blue-500`) or Bootstrap primary as the brand colour.
|
|
86
|
+
- Heavy drop shadows, borders AND shadows AND gradients on the same element.
|
|
87
|
+
- Tiny low-contrast grey text; ALL CAPS paragraphs; more than two font families.
|
|
88
|
+
|
|
89
|
+
## 6. Build, then review like a designer
|
|
90
|
+
|
|
91
|
+
After building, check honestly and fix before reporting done:
|
|
92
|
+
|
|
93
|
+
1. Squint test: is the hierarchy obvious — what is first, second, third?
|
|
94
|
+
2. Is the spacing consistent and generous? Any cramped block or misaligned edge?
|
|
95
|
+
3. Does the palette hold together — one accent, tinted neutrals, contrast passes?
|
|
96
|
+
4. Is there one memorable, distinctive element? Would a designer recognise a direction?
|
|
97
|
+
5. Every interactive element has hover and focus states; works at 375px wide.
|
|
98
|
+
6. Nothing from section 5 slipped in.
|
|
99
|
+
|
|
100
|
+
If a browser or screenshot tool is available, look at the result and iterate. Deliver
|
|
101
|
+
single-file HTML with inline CSS unless the project has its own structure; load fonts from
|
|
102
|
+
Google Fonts with `display=swap` and give a system fallback stack.
|
package/src/skills.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import os from 'node:os';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
// Skills that ship with Jaitus. A project or personal skill with the same name wins.
|
|
14
|
+
const BUILTIN = path.join(path.dirname(fileURLToPath(import.meta.url)), 'skills', 'builtin');
|
|
11
15
|
|
|
12
16
|
const MAX_SKILL = 32_000;
|
|
13
17
|
|
|
@@ -47,6 +51,7 @@ export function discoverSkills(cwd) {
|
|
|
47
51
|
[path.join(cwd, '.claude', 'skills'), 'project'],
|
|
48
52
|
[path.join(os.homedir(), '.jaitus', 'skills'), 'user'],
|
|
49
53
|
[path.join(os.homedir(), '.claude', 'skills'), 'user'],
|
|
54
|
+
[BUILTIN, 'builtin'],
|
|
50
55
|
];
|
|
51
56
|
const skills = new Map();
|
|
52
57
|
for (const [root, scope] of roots) {
|
|
@@ -58,5 +63,5 @@ export function discoverSkills(cwd) {
|
|
|
58
63
|
export function skillCatalog(skills) {
|
|
59
64
|
const entries = Object.values(skills || {});
|
|
60
65
|
if (!entries.length) return '';
|
|
61
|
-
return `\n\
|
|
66
|
+
return `\n\nSkills — instructions for particular kinds of work. When the task matches one, call load_skill with its name before you start, and follow it. The user can also run one as /<name>.\n${entries.map((s) => `- ${s.name}: ${s.description}`).join('\n')}`;
|
|
62
67
|
}
|