@cspeach/cli 1.1.33 → 1.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/anthropic-provider.js +2 -0
- package/dist/agent/azure-profile.js +7 -0
- package/dist/agent/connection-profile.js +49 -0
- package/dist/agent/loop.js +182 -39
- package/dist/agent/provider-shape.js +36 -0
- package/dist/agent/providers/ai-hub-provider.js +6 -5
- package/dist/agent/providers/azure-provider.js +125 -0
- package/dist/agent/providers/byok-provider.js +8 -7
- package/dist/agent/providers/factory.js +8 -3
- package/dist/agent/providers/local-provider.js +18 -22
- package/dist/agent/risk-gate.js +202 -0
- package/dist/agent/summarise-via-provider.js +55 -8
- package/dist/agent/tool-dispatch.js +19 -0
- package/dist/agent/turn-error-ux.js +22 -0
- package/dist/auth/api-key.js +10 -5
- package/dist/auth/auth-file.js +35 -6
- package/dist/auth/azure/azure-key.js +21 -0
- package/dist/auth/azure/azure-log.js +45 -0
- package/dist/auth/azure/cache-store.js +171 -0
- package/dist/auth/azure/clear.js +28 -0
- package/dist/auth/azure/loopback.js +192 -0
- package/dist/auth/azure/msal-client.js +41 -0
- package/dist/auth/azure/sign-in.js +219 -0
- package/dist/auth/azure/startup-sign-in.js +64 -0
- package/dist/auth/azure/token-provider.js +116 -0
- package/dist/auth/key-resolver.js +258 -0
- package/dist/auth/me.js +11 -5
- package/dist/cli.js +21 -2
- package/dist/commands/compact.js +12 -1
- package/dist/commands/config-set.js +74 -3
- package/dist/commands/config-show.js +25 -0
- package/dist/commands/cost.js +19 -6
- package/dist/commands/help.js +2 -2
- package/dist/commands/login-azure.js +33 -0
- package/dist/commands/login.js +56 -8
- package/dist/commands/logout.js +34 -10
- package/dist/commands/plan-boundary.js +4 -4
- package/dist/commands/plan-model-tier.js +8 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/whoami.js +113 -9
- package/dist/config/loader.js +5 -0
- package/dist/connection/azure-auth.js +135 -0
- package/dist/connection/azure-errors.js +103 -0
- package/dist/connection/azure-facts.js +21 -0
- package/dist/connection/azure-gate.js +40 -0
- package/dist/connection/azure-last-call.js +5 -0
- package/dist/connection/azure-turn-error.js +25 -0
- package/dist/connection/cache.js +70 -0
- package/dist/connection/fetch.js +33 -0
- package/dist/connection/guard.js +94 -0
- package/dist/connection/limit-gate.js +26 -0
- package/dist/connection/local-usage.js +71 -0
- package/dist/connection/login-step.js +62 -0
- package/dist/connection/startup.js +67 -0
- package/dist/connection/state.js +68 -0
- package/dist/connection/types.js +1 -0
- package/dist/cost/cost-log.js +3 -1
- package/dist/cost/credits-wire.js +1 -1
- package/dist/cost/credits.js +4 -0
- package/dist/cost/display-mode.js +7 -0
- package/dist/cost/router-spend.js +3 -0
- package/dist/cost/session-cost.js +4 -0
- package/dist/doctor/checks/azure.js +100 -0
- package/dist/doctor/checks/credits.js +4 -3
- package/dist/doctor/checks/usage-reporting.js +22 -0
- package/dist/doctor/run.js +7 -0
- package/dist/lib/graceful-exit.js +17 -0
- package/dist/licence/licence-check.js +205 -0
- package/dist/licence/paths.js +6 -0
- package/dist/licence/turn-gate.js +66 -0
- package/dist/models/model-identity.js +65 -0
- package/dist/models/resolve.js +8 -0
- package/dist/models/server-config.js +3 -2
- package/dist/one-shot.js +86 -2
- package/dist/projects/answer-blockers.js +25 -12
- package/dist/renderer/change-card.js +28 -4
- package/dist/renderer/footer-line.js +7 -1
- package/dist/renderer/notices.js +27 -13
- package/dist/renderer/progress-chatter.js +3 -1
- package/dist/renderer/status-footer.js +13 -6
- package/dist/renderer/tool-labels.js +8 -2
- package/dist/renderer/verify-chain.js +41 -18
- package/dist/renderer/wrap-rows.js +87 -0
- package/dist/repl/ask-or-no.js +18 -0
- package/dist/repl/builtin-commands.js +10 -2
- package/dist/repl/post-turn-status.js +3 -2
- package/dist/repl/safety-confirm.js +47 -2
- package/dist/repl/session-spend-line.js +4 -1
- package/dist/repl/startup-account.js +51 -0
- package/dist/repl.js +217 -48
- package/dist/router/byok-classifier.js +41 -14
- package/dist/router/classifier.js +5 -16
- package/dist/router/router-target.js +41 -0
- package/dist/sap/connection-manager.js +15 -0
- package/dist/sap/onboarding.js +70 -64
- package/dist/sap/session-end.js +66 -0
- package/dist/session/audit-export.js +11 -4
- package/dist/skills/manifest-client.js +37 -4
- package/dist/skills/skills-cache.js +115 -0
- package/dist/skills/source-authed.js +276 -0
- package/dist/tools/ask-question.js +1 -1
- package/dist/tools/filesystem/glob.js +12 -3
- package/dist/tools/filesystem/grep.js +14 -3
- package/dist/tools/sap-read.js +22 -22
- package/dist/tools/sap-write.js +249 -18
- package/dist/tools/snapshot.js +26 -0
- package/dist/tools/tool-loading.js +9 -10
- package/dist/ui/app.js +19 -9
- package/dist/ui/approval-modal.js +37 -5
- package/dist/ui/live-area.js +14 -1
- package/dist/ui/widgets/ask-card.js +183 -107
- package/dist/ui/widgets/coaching-picker.js +22 -1
- package/dist/ui/widgets/confirm-card.js +102 -76
- package/dist/usage/usage-queue.js +323 -0
- package/dist/usage/usage-reporter.js +270 -0
- package/package.json +4 -3
- package/dist/skills/bundled-skills.js +0 -286
- package/dist/skills/source-bundled.js +0 -20
- package/dist/skills/source-manifest.js +0 -36
package/dist/tools/sap-write.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* unlock(type, name, lockHandle) → Promise<void> line 1949
|
|
17
17
|
* serviceBindingPublish(name) → Promise<{status,error?}> line 2658
|
|
18
18
|
* serviceBindingUnpublish(name) → Promise<{status,error?}>
|
|
19
|
-
*
|
|
19
|
+
* maintainMessages({name, messages, transport?}) → Promise<MaintainMessagesResult> (0.1.19)
|
|
20
20
|
* numberRangeIntervals (STUB — not yet in sap-client)
|
|
21
21
|
*
|
|
22
22
|
* auditLog aliased from safety/audit-log.ts#log via sap-client/src/index.ts.
|
|
@@ -26,7 +26,7 @@ import { readFileSync } from 'node:fs';
|
|
|
26
26
|
import { registerTool } from './index.js';
|
|
27
27
|
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
28
28
|
import { grantClaimMismatch, grantActivateMismatch, grantOwningTransportMismatch, grantOwningTransportDetail } from '../approvals/grant-claims.js';
|
|
29
|
-
import { autoSnapshot, autoSnapshotClassInclude } from './snapshot.js';
|
|
29
|
+
import { autoSnapshot, autoSnapshotClassInclude, autoSnapshotMessageClass } from './snapshot.js';
|
|
30
30
|
import { includeSnapshotType } from './include-snapshot.js';
|
|
31
31
|
import { buildWriteChainLine } from '../renderer/verify-chain.js';
|
|
32
32
|
import { buildChangeCard } from '../renderer/change-card.js';
|
|
@@ -49,6 +49,7 @@ import { noteWriteTransport, setCurrentTransport } from '../repl/current-transpo
|
|
|
49
49
|
import { markTransportCreatedThisSession } from './transport.js';
|
|
50
50
|
import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
|
|
51
51
|
import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
|
|
52
|
+
import { noteManualLock, noteManualUnlock } from '../sap/session-end.js';
|
|
52
53
|
// ── Transcript visibility helpers (D16-display + tour slide-7, 2026-06-12) ──
|
|
53
54
|
//
|
|
54
55
|
// Rule 7 auto-snapshots always fired but the transcript never said so
|
|
@@ -868,7 +869,11 @@ registerTool({
|
|
|
868
869
|
object: result.objectName,
|
|
869
870
|
type: result.objectType,
|
|
870
871
|
package: result.packageName,
|
|
871
|
-
|
|
872
|
+
// A message class has no source and is never activated: its messages
|
|
873
|
+
// go in through sap_message_maintain (bug 3, 2026-10-12).
|
|
874
|
+
next_step: String(result.objectType ?? '').toUpperCase() === 'MSAG'
|
|
875
|
+
? 'add_messages_via_sap_message_maintain (a message class has no source and no activation)'
|
|
876
|
+
: 'write_source_via_sap_set_source_then_activate',
|
|
872
877
|
}),
|
|
873
878
|
};
|
|
874
879
|
},
|
|
@@ -1573,6 +1578,9 @@ registerTool({
|
|
|
1573
1578
|
},
|
|
1574
1579
|
handler: async (args, ctx) => {
|
|
1575
1580
|
const result = await ctx.adt.lock(args.type, args.name);
|
|
1581
|
+
// The turn-end session close keeps the session while this lock is held.
|
|
1582
|
+
if (result?.status === 'locked')
|
|
1583
|
+
noteManualLock();
|
|
1576
1584
|
return { content: JSON.stringify(result) };
|
|
1577
1585
|
},
|
|
1578
1586
|
});
|
|
@@ -1596,6 +1604,7 @@ registerTool({
|
|
|
1596
1604
|
},
|
|
1597
1605
|
handler: async (args, ctx) => {
|
|
1598
1606
|
await ctx.adt.unlock(args.type, args.name, args.lockHandle);
|
|
1607
|
+
noteManualUnlock();
|
|
1599
1608
|
return { content: JSON.stringify({ status: 'ok' }) };
|
|
1600
1609
|
},
|
|
1601
1610
|
});
|
|
@@ -1644,34 +1653,256 @@ registerTool({
|
|
|
1644
1653
|
return { content: JSON.stringify(result) };
|
|
1645
1654
|
},
|
|
1646
1655
|
});
|
|
1647
|
-
// ── sap_message_maintain
|
|
1656
|
+
// ── sap_message_maintain ──────────────────────────────────────────────────────
|
|
1648
1657
|
//
|
|
1649
|
-
//
|
|
1650
|
-
//
|
|
1658
|
+
// Signature (sap-client 0.1.19): maintainMessages({name, messages, transport?})
|
|
1659
|
+
// → Promise<MaintainMessagesResult>
|
|
1660
|
+
// Adds or changes T100 messages in an existing message class, in one batch.
|
|
1661
|
+
// sap-client merges into the existing rows, locks, PUTs the whole class,
|
|
1662
|
+
// unlocks in a finally and READS BACK: success is `verified`, never HTTP 200.
|
|
1663
|
+
// Rule 7 snapshot = the class document (all messages) before the write.
|
|
1664
|
+
// No syntax check and no activation: a message class is not activatable.
|
|
1665
|
+
const MESSAGE_TEXT_MAX = 73;
|
|
1666
|
+
/** Same rule as sap-client's isValidMessageClassName (optional /NAMESPACE/). */
|
|
1667
|
+
const MESSAGE_CLASS_NAME_RE = /^(?:\/[A-Z0-9_]{1,10}\/)?[A-Z0-9_]{1,20}$/;
|
|
1668
|
+
/** Review M-1: a plain sentence when SAP may still hold a lock, else undefined. */
|
|
1669
|
+
function lockWarning(className, r) {
|
|
1670
|
+
if (r.released !== false)
|
|
1671
|
+
return undefined;
|
|
1672
|
+
const what = (r.unlockFailures ?? []).join(', ') || 'unlock';
|
|
1673
|
+
return `SAP may still hold a lock on message class ${className} (failed: ${what}). Check SM12 and remove it there, or it clears when the session ends.`;
|
|
1674
|
+
}
|
|
1675
|
+
/** The rows from the tool args (`messages`, or the single-message short form). */
|
|
1676
|
+
function messageRowsFromArgs(args) {
|
|
1677
|
+
const list = Array.isArray(args['messages']) ? args['messages'] : [];
|
|
1678
|
+
const rows = list.map((m) => ({
|
|
1679
|
+
msgno: String(m?.['msgno'] ?? m?.['number'] ?? '').trim(),
|
|
1680
|
+
text: typeof m?.['text'] === 'string' ? m['text'] : '',
|
|
1681
|
+
...(typeof m?.['selfExplanatory'] === 'boolean' ? { selfExplanatory: m['selfExplanatory'] } : {}),
|
|
1682
|
+
}));
|
|
1683
|
+
if (rows.length === 0 && args['messageNumber'] !== undefined) {
|
|
1684
|
+
rows.push({
|
|
1685
|
+
msgno: String(args['messageNumber']).trim(),
|
|
1686
|
+
text: typeof args['messageText'] === 'string' ? args['messageText'] : '',
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
return rows;
|
|
1690
|
+
}
|
|
1691
|
+
/** Same rules as sap-client's validateMessageUpserts — checked here so a bad
|
|
1692
|
+
* request never spends the approval. A plain sentence, or null. */
|
|
1693
|
+
function messageRowsProblem(rows) {
|
|
1694
|
+
if (rows.length === 0)
|
|
1695
|
+
return 'Give at least one message to add or change.';
|
|
1696
|
+
const seen = new Set();
|
|
1697
|
+
for (const r of rows) {
|
|
1698
|
+
if (!/^\d{3}$/.test(r.msgno))
|
|
1699
|
+
return `Message number "${r.msgno}" is not valid. Use three digits, 000 to 999.`;
|
|
1700
|
+
if (seen.has(r.msgno))
|
|
1701
|
+
return `Message ${r.msgno} is in the request more than once.`;
|
|
1702
|
+
seen.add(r.msgno);
|
|
1703
|
+
if (r.text.trim().length === 0)
|
|
1704
|
+
return `Message ${r.msgno} has an empty text.`;
|
|
1705
|
+
const len = Array.from(r.text).length;
|
|
1706
|
+
if (len > MESSAGE_TEXT_MAX)
|
|
1707
|
+
return `Message ${r.msgno} is ${len} characters long. SAP allows ${MESSAGE_TEXT_MAX}.`;
|
|
1708
|
+
}
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1651
1711
|
registerTool({
|
|
1652
1712
|
name: 'sap_message_maintain',
|
|
1653
|
-
description: '
|
|
1654
|
-
+ '
|
|
1655
|
-
+ '
|
|
1713
|
+
description: 'Add or change messages in an existing message class (MSAG, the T100 texts behind SE91). '
|
|
1714
|
+
+ 'Create the class first with sap_create_object type MSAG. One call takes many messages. '
|
|
1715
|
+
+ 'Each message: msgno (three digits, 000-999) and text (up to 73 characters, &1-&4 placeholders allowed). '
|
|
1716
|
+
+ 'Messages not in the call are kept. No source, no syntax check, no activation. '
|
|
1717
|
+
+ 'The type (E, W, I, S) is not stored: it is chosen where the message is raised, e.g. MESSAGE e001(zfoo). '
|
|
1718
|
+
+ 'Requires approval_id.',
|
|
1656
1719
|
isMutating: true,
|
|
1657
1720
|
requiresSap: true,
|
|
1658
1721
|
input_schema: {
|
|
1659
1722
|
type: 'object',
|
|
1660
1723
|
properties: {
|
|
1661
|
-
messageClass: { type: 'string', description: 'Message class name (e.g.
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1724
|
+
messageClass: { type: 'string', description: 'Message class name (e.g. ZSALES_MSG)' },
|
|
1725
|
+
messages: {
|
|
1726
|
+
type: 'array',
|
|
1727
|
+
description: 'Messages to add or change',
|
|
1728
|
+
items: {
|
|
1729
|
+
type: 'object',
|
|
1730
|
+
properties: {
|
|
1731
|
+
msgno: { type: 'string', description: 'Three-digit message number (e.g. 001)' },
|
|
1732
|
+
text: { type: 'string', description: 'Message text, up to 73 characters (e.g. "Order &1 not found")' },
|
|
1733
|
+
selfExplanatory: { type: 'boolean', description: 'Optional. New messages default to true (no long text).' },
|
|
1734
|
+
},
|
|
1735
|
+
required: ['msgno', 'text'],
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
messageNumber: { type: 'string', description: 'Short form for one message: the number (use messages for several)' },
|
|
1739
|
+
messageText: { type: 'string', description: 'Short form for one message: the text' },
|
|
1740
|
+
transport: { type: 'string', description: 'Transport request number (optional)' },
|
|
1741
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
1665
1742
|
},
|
|
1666
|
-
required: ['messageClass', '
|
|
1743
|
+
required: ['messageClass', 'approval_id'],
|
|
1667
1744
|
},
|
|
1668
|
-
handler: async (
|
|
1745
|
+
handler: async (args, ctx) => {
|
|
1746
|
+
const className = String(args.messageClass ?? '').trim().toUpperCase();
|
|
1747
|
+
// ── Input check — before the approval is spent ──────────────────────────
|
|
1748
|
+
const rows = messageRowsFromArgs(args);
|
|
1749
|
+
const problem = !className
|
|
1750
|
+
? 'Give the message class name.'
|
|
1751
|
+
: !MESSAGE_CLASS_NAME_RE.test(className)
|
|
1752
|
+
? `"${String(args.messageClass)}" is not a valid message class name (letters, digits and _ only, 20 characters at most).`
|
|
1753
|
+
: messageRowsProblem(rows);
|
|
1754
|
+
if (problem) {
|
|
1755
|
+
return { content: JSON.stringify({ error: 'invalid_messages', detail: problem }), is_error: true };
|
|
1756
|
+
}
|
|
1757
|
+
// ── Capability — before the approval is spent (review I-1) ──────────────
|
|
1758
|
+
const adt = ctx.adt;
|
|
1759
|
+
if (typeof adt.readMessageClass !== 'function' || typeof adt.maintainMessages !== 'function') {
|
|
1760
|
+
return {
|
|
1761
|
+
content: JSON.stringify({
|
|
1762
|
+
error: 'message_maintain_unavailable',
|
|
1763
|
+
detail: 'This CSPeach build cannot write messages yet. Update CSPeach and try again.',
|
|
1764
|
+
}),
|
|
1765
|
+
is_error: true,
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
const msagClient = adt;
|
|
1769
|
+
// ── Gate 1: Approval ────────────────────────────────────────────────────
|
|
1770
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.messageClass, 'modify');
|
|
1771
|
+
if (!verify.ok) {
|
|
1772
|
+
return {
|
|
1773
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
1774
|
+
is_error: true,
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
// 2026-10-04 — a plan-grant id holds the write to its claims (type, package, transport).
|
|
1778
|
+
{
|
|
1779
|
+
const claim = grantClaimMismatch(verify.payload, { type: 'MSAG', transport: args.transport ?? ctx.currentTransport?.get() ?? null });
|
|
1780
|
+
if (claim)
|
|
1781
|
+
return { content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: claim }), is_error: true };
|
|
1782
|
+
}
|
|
1783
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
1784
|
+
const startedAt = Date.now();
|
|
1785
|
+
// ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
|
|
1786
|
+
{
|
|
1787
|
+
const sessionTransport = ctx.currentTransport?.get() ?? null;
|
|
1788
|
+
if (args.transport && sessionTransport && args.transport !== sessionTransport) {
|
|
1789
|
+
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
|
|
1793
|
+
const resolved = await resolveWriteTransport(ctx, 'MSAG', className, effectiveTransport);
|
|
1794
|
+
// 2026-10-04 — a plan-grant id never writes into a transport other than the grant's.
|
|
1795
|
+
if (grantOwningTransportMismatch(verify.payload, resolved.owning)) {
|
|
1796
|
+
return {
|
|
1797
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: 'transport_mismatch', detail: grantOwningTransportDetail(className, resolved.owning) }),
|
|
1798
|
+
is_error: true,
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
// ── Gate 2: Snapshot (Rule 7) — HARD gate, of the whole class ────────────
|
|
1802
|
+
let snapOutcome;
|
|
1803
|
+
try {
|
|
1804
|
+
snapOutcome = await autoSnapshotMessageClass(className, msagClient);
|
|
1805
|
+
}
|
|
1806
|
+
catch (err) {
|
|
1807
|
+
return {
|
|
1808
|
+
content: JSON.stringify({ error: ERR.SNAPSHOT_FAILED, detail: String(err) }),
|
|
1809
|
+
is_error: true,
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
if (snapOutcome.reason === 'object_not_found') {
|
|
1813
|
+
return {
|
|
1814
|
+
content: JSON.stringify({
|
|
1815
|
+
error: 'message_class_not_found',
|
|
1816
|
+
detail: `Message class ${className} does not exist. Create it first with sap_create_object type MSAG, then add the messages.`,
|
|
1817
|
+
}),
|
|
1818
|
+
is_error: true,
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
if (!snapOutcome.taken) {
|
|
1822
|
+
return {
|
|
1823
|
+
content: JSON.stringify({
|
|
1824
|
+
error: ERR.SNAPSHOT_FAILED,
|
|
1825
|
+
reason: snapOutcome.reason ?? 'source_read_failed',
|
|
1826
|
+
detail: snapOutcome.detail,
|
|
1827
|
+
recovery: 'The message class could not be read, so no snapshot was possible and nothing was written. If this is a connection or sign-in problem, fix it and try again.',
|
|
1828
|
+
}),
|
|
1829
|
+
is_error: true,
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
emitSnapshotNotice(ctx, snapOutcome, 'MSAG', className);
|
|
1833
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
1834
|
+
await appendPending(ctx.session, {
|
|
1835
|
+
tool_use_id: toolUseId,
|
|
1836
|
+
tool: 'sap_message_maintain',
|
|
1837
|
+
args: { messageClass: className, messages: rows.map((r) => r.msgno) },
|
|
1838
|
+
started_at: new Date().toISOString(),
|
|
1839
|
+
});
|
|
1840
|
+
// ── Write (sap-client locks, PUTs, unlocks and reads back) ───────────────
|
|
1841
|
+
let result;
|
|
1842
|
+
try {
|
|
1843
|
+
result = await msagClient.maintainMessages({
|
|
1844
|
+
name: className,
|
|
1845
|
+
messages: rows,
|
|
1846
|
+
...(resolved.transport ? { transport: resolved.transport } : {}),
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
catch (err) {
|
|
1850
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1851
|
+
await finalizeToolCall(ctx.session, toolUseId, detail, true, Date.now() - startedAt);
|
|
1852
|
+
return {
|
|
1853
|
+
content: JSON.stringify({ error: 'message_maintain_failed', detail }),
|
|
1854
|
+
is_error: true,
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
const lockNote = lockWarning(className, result);
|
|
1858
|
+
if (lockNote)
|
|
1859
|
+
ctx.chunkEmitter?.emit('warn', lockNote);
|
|
1860
|
+
if (!result.verified) {
|
|
1861
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
1862
|
+
return {
|
|
1863
|
+
content: JSON.stringify({
|
|
1864
|
+
error: 'message_maintain_failed',
|
|
1865
|
+
...(lockNote ? { lock_warning: lockNote } : {}),
|
|
1866
|
+
detail: result.error ?? 'The messages could not be confirmed in SAP.',
|
|
1867
|
+
missing: result.missing,
|
|
1868
|
+
...(result.httpStatus !== undefined ? { http_status: result.httpStatus } : {}),
|
|
1869
|
+
...(result.lockedInTransport ? {
|
|
1870
|
+
locked_in_transport: result.lockedInTransport,
|
|
1871
|
+
recovery: `Retry this exact call with transport=${result.lockedInTransport} — that request already owns the message class.`,
|
|
1872
|
+
} : {}),
|
|
1873
|
+
}),
|
|
1874
|
+
is_error: true,
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
if (result.noChange) {
|
|
1878
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ unchanged: true }), false, Date.now() - startedAt);
|
|
1879
|
+
return {
|
|
1880
|
+
content: JSON.stringify({
|
|
1881
|
+
written: false,
|
|
1882
|
+
unchanged: true,
|
|
1883
|
+
message_class: className,
|
|
1884
|
+
changes: result.changes,
|
|
1885
|
+
message: `Message class ${className} already has these texts. Nothing was changed.`,
|
|
1886
|
+
}),
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
// Audit only after SAP's read-back confirmed every message.
|
|
1890
|
+
await auditLog('maintainMessages', 'MSAG', className, 'success', Date.now() - startedAt, result.changes.map((c) => `${c.msgno}:${c.action}`).join(' '));
|
|
1891
|
+
noteWriteTransport(resolved.transport); // F-L2-1 statusline wire
|
|
1892
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ written: true, verified: true, changes: result.changes }), false, Date.now() - startedAt);
|
|
1893
|
+
const written = result.changes.filter((c) => c.action !== 'unchanged').map((c) => c.msgno);
|
|
1894
|
+
ctx.chunkEmitter?.emit('info', `messages ✓ ${className} ${written.join(' ')} (no activation needed)`);
|
|
1895
|
+
const first = written[0] ?? rows[0].msgno;
|
|
1669
1896
|
return {
|
|
1670
1897
|
content: JSON.stringify({
|
|
1671
|
-
|
|
1672
|
-
|
|
1898
|
+
written: true,
|
|
1899
|
+
verified: true,
|
|
1900
|
+
message_class: className,
|
|
1901
|
+
changes: result.changes,
|
|
1902
|
+
...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
|
|
1903
|
+
...(lockNote ? { lock_warning: lockNote } : {}),
|
|
1904
|
+
next_step: `Done — no activation needed. Raise these from code by number, e.g. MESSAGE e${first}(${className.toLowerCase()}) WITH …, or a T100 textid in an exception class. Never hard-code the texts.`,
|
|
1673
1905
|
}),
|
|
1674
|
-
is_error: true,
|
|
1675
1906
|
};
|
|
1676
1907
|
},
|
|
1677
1908
|
});
|
package/dist/tools/snapshot.js
CHANGED
|
@@ -127,6 +127,32 @@ export async function autoSnapshotClassInclude(className, includeType, adt) {
|
|
|
127
127
|
const entry = await snapshots.take(includeSnapshotType(includeType), className, src, 'before_write');
|
|
128
128
|
return { taken: true, entry };
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Rule-7 snapshot of a message class, taken before sap_message_maintain
|
|
132
|
+
* changes it. A message class has no source: the snapshot is the whole class
|
|
133
|
+
* document as SAP serves it (every message), stored under type MSAG.
|
|
134
|
+
*
|
|
135
|
+
* 404 → 'object_not_found' (no such class — the write cannot go ahead)
|
|
136
|
+
* other read error → 'source_read_failed' (+ detail) — the write is blocked
|
|
137
|
+
* read OK → snapshot taken
|
|
138
|
+
* A snapshots.take failure THROWS, like autoSnapshot.
|
|
139
|
+
*/
|
|
140
|
+
export async function autoSnapshotMessageClass(name, adt) {
|
|
141
|
+
let xml;
|
|
142
|
+
try {
|
|
143
|
+
xml = (await adt.readMessageClass(name)).xml;
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (isNotFound(err))
|
|
147
|
+
return { taken: false, reason: 'object_not_found' };
|
|
148
|
+
return { taken: false, reason: 'source_read_failed', detail: shortErrorMessage(err) };
|
|
149
|
+
}
|
|
150
|
+
if (typeof xml !== 'string' || xml.length === 0) {
|
|
151
|
+
return { taken: false, reason: 'source_read_failed', detail: 'the message class read returned no document' };
|
|
152
|
+
}
|
|
153
|
+
const entry = await snapshots.take('MSAG', name.toUpperCase(), xml, 'before_write');
|
|
154
|
+
return { taken: true, entry };
|
|
155
|
+
}
|
|
130
156
|
// ── sap_snapshot_list ─────────────────────────────────────────────────────────
|
|
131
157
|
registerTool({
|
|
132
158
|
name: 'sap_snapshot_list',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toAnthropicTools } from './index.js';
|
|
2
|
+
import { profileForMode } from '../agent/connection-profile.js';
|
|
2
3
|
/** The server-side tool search tool (probe P6: this exact name). */
|
|
3
4
|
export const TOOL_SEARCH_TOOL = Object.freeze({
|
|
4
5
|
type: 'tool_search_tool_regex_20251119',
|
|
@@ -105,25 +106,23 @@ export function modelAcceptsDeferredTools(model) {
|
|
|
105
106
|
return DEFERRED_TOOL_MODELS.has(model.trim().toLowerCase().replace(/-\d{8}$/, ''));
|
|
106
107
|
}
|
|
107
108
|
/**
|
|
108
|
-
* Mode precedence (ruling F13)
|
|
109
|
-
*
|
|
110
|
-
* (`deferred` | `static`)
|
|
111
|
-
* '
|
|
112
|
-
*
|
|
109
|
+
* Mode precedence (ruling F13), read from the connection profile since the
|
|
110
|
+
* Azure tenant mode (Task 1): 'static' profiles (AI-hub, local, azure v0) are
|
|
111
|
+
* always 'static'; otherwise env `CSPEACH_TOOL_LOADING` (`deferred` | `static`)
|
|
112
|
+
* wins; then 'served' profiles (managed, BYOK) use the served value (default
|
|
113
|
+
* 'static'); an unknown/fake mode ('env-only') stays 'static'.
|
|
113
114
|
*/
|
|
114
115
|
export function resolveToolLoading(i) {
|
|
115
|
-
|
|
116
|
+
const rule = (i.profile ?? profileForMode(i.providerMode)).deferredTools;
|
|
117
|
+
if (rule === 'static')
|
|
116
118
|
return 'static';
|
|
117
119
|
const raw = i.env.CSPEACH_TOOL_LOADING?.trim().toLowerCase();
|
|
118
120
|
if (raw === 'deferred' || raw === 'static')
|
|
119
121
|
return raw;
|
|
120
122
|
// Task 21 fix round (controller ruling): an old proxy serves no tool_loading
|
|
121
123
|
// and adds none of the deferred-tool betas, so 'deferred' only when served.
|
|
122
|
-
if (
|
|
124
|
+
if (rule === 'served')
|
|
123
125
|
return i.served ?? 'static';
|
|
124
|
-
if (i.providerMode === 'byok')
|
|
125
|
-
return i.served ?? 'static';
|
|
126
|
-
// Unknown/fake provider modes keep today's full list.
|
|
127
126
|
return 'static';
|
|
128
127
|
}
|
|
129
128
|
/**
|
package/dist/ui/app.js
CHANGED
|
@@ -6,7 +6,7 @@ import { roleProps } from './role-props.js';
|
|
|
6
6
|
import { IDLE_EXIT_WINDOW_MS } from './interrupt.js';
|
|
7
7
|
import { Body } from './body.js';
|
|
8
8
|
import { Footer } from './footer.js';
|
|
9
|
-
import { ApprovalModal } from './approval-modal.js';
|
|
9
|
+
import { ApprovalModal, estimateApprovalModalRows } from './approval-modal.js';
|
|
10
10
|
import { CommandPalette } from './command-palette.js';
|
|
11
11
|
import { FilePalette } from './file-palette.js';
|
|
12
12
|
import { nextResolutionStep, applySlashPick } from './line-resolution.js';
|
|
@@ -15,7 +15,7 @@ import { FooterLine } from './footer-line.js';
|
|
|
15
15
|
import { ShortcutsSheet } from './widgets/shortcuts-sheet.js';
|
|
16
16
|
import { wasPasted } from '../repl/paste-marker.js';
|
|
17
17
|
import { turnStatusEmitter } from './turn-status-emitter.js';
|
|
18
|
-
import { pickSlotOccupant, liveBudget, SLOT_ROWS_ESTIMATE, PALETTE_ROWS } from './live-area.js';
|
|
18
|
+
import { pickSlotOccupant, liveBudget, cardRowBudget, SLOT_ROWS_ESTIMATE, PALETTE_ROWS } from './live-area.js';
|
|
19
19
|
import { Checklist } from './checklist.js';
|
|
20
20
|
import { formatChecklist, isPlanFinished, CHECKLIST_MAX_ROWS } from '../renderer/checklist-format.js';
|
|
21
21
|
import { listWorkspaceFileChoices } from '../repl/at-picker.js';
|
|
@@ -27,7 +27,7 @@ import { approvalEmitter } from './approval-emitter.js';
|
|
|
27
27
|
import { useHoldRawMode } from './widgets/hold-raw-mode.js';
|
|
28
28
|
import { SapStateContext, globalStore } from './sap-state-store.js';
|
|
29
29
|
import { coachingPickerEmitter } from './coaching-picker-emitter.js';
|
|
30
|
-
import { CoachingPicker } from './widgets/coaching-picker.js';
|
|
30
|
+
import { CoachingPicker, estimateCoachingPickerRows } from './widgets/coaching-picker.js';
|
|
31
31
|
import { askQuestionEmitter } from './ask-question-emitter.js';
|
|
32
32
|
import { AskCard, estimateAskCardRows } from './widgets/ask-card.js';
|
|
33
33
|
import { ConfirmCard, estimateConfirmCardRows } from './widgets/confirm-card.js';
|
|
@@ -575,14 +575,24 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
575
575
|
// contributes a fixed estimate. Q2 (ask) and C1 (confirm) replace theirs
|
|
576
576
|
// with a real estimator right below this line. P2 replaces `checklistRows: 0`.
|
|
577
577
|
let cardRows = occupant === null ? 0 : SLOT_ROWS_ESTIMATE[occupant];
|
|
578
|
-
|
|
579
|
-
//
|
|
578
|
+
const paletteRows = lineResolution ? PALETTE_ROWS : 0;
|
|
579
|
+
// Fix round 1 (review I1): the room a card may take beside one input line.
|
|
580
|
+
const askMaxRows = cardRowBudget({ rows, paletteRows });
|
|
581
|
+
// Q2 — the ask card's height after its own folds under askMaxRows (labels
|
|
582
|
+
// to 1 row, descriptions to 1 row, then none — askCardLayout). Computed
|
|
583
|
+
// with descriptions ON, so shedding them cannot oscillate.
|
|
580
584
|
if (occupant === 'ask' && askQuestionPending)
|
|
581
|
-
cardRows = estimateAskCardRows(askQuestionPending, true, uiWidth(columns));
|
|
585
|
+
cardRows = estimateAskCardRows(askQuestionPending, true, uiWidth(columns), askMaxRows);
|
|
582
586
|
// C1 — the confirm card's real height (facts + choices) for the spec §2 cap.
|
|
583
587
|
if (occupant === 'confirm' && confirmSlot.pending)
|
|
584
588
|
cardRows = estimateConfirmCardRows(confirmSlot.pending, uiWidth(columns), rows - 6);
|
|
585
|
-
|
|
589
|
+
// Fix batch 2026-10-12 — "Pick if:" wraps; count every row.
|
|
590
|
+
if (occupant === 'coaching' && coachingPending)
|
|
591
|
+
cardRows = estimateCoachingPickerRows(coachingPending.params, uiWidth(columns));
|
|
592
|
+
// Fix batch 2026-10-12 — the approval card's risk reasons wrap; count every
|
|
593
|
+
// row (+1: the slot's top margin).
|
|
594
|
+
if (occupant === 'approval' && pendingApproval)
|
|
595
|
+
cardRows = 1 + estimateApprovalModalRows(pendingApproval, uiWidth(columns) - 2);
|
|
586
596
|
// P2 — the checklist's WANTED height (header + rows); liveBudget folds it.
|
|
587
597
|
const showChecklist = todos.length > 0 && !planClosed;
|
|
588
598
|
const checklistWidth = uiWidth(columns);
|
|
@@ -597,7 +607,7 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
597
607
|
const budget = liveBudget({ rows, checklistRows: checklistDesiredRows, cardRows, inputLines, paletteRows });
|
|
598
608
|
let slot = null;
|
|
599
609
|
if (occupant === 'approval' && pendingApproval) {
|
|
600
|
-
slot = (_jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsx(ApprovalModal, { change: pendingApproval.change, effectiveRisk: pendingApproval.effectiveRisk, transport: pendingApproval.transport, onApply: () => {
|
|
610
|
+
slot = (_jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsx(ApprovalModal, { change: pendingApproval.change, effectiveRisk: pendingApproval.effectiveRisk, transport: pendingApproval.transport, width: uiWidth(columns) - 2, onApply: () => {
|
|
601
611
|
pendingApproval.resolve({ approved: true });
|
|
602
612
|
clearApproval(pendingApproval);
|
|
603
613
|
}, onSkip: () => {
|
|
@@ -614,7 +624,7 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
614
624
|
}
|
|
615
625
|
else if (occupant === 'ask' && askQuestionPending) {
|
|
616
626
|
const req = askQuestionPending;
|
|
617
|
-
slot = (_jsx(AskCard, { request: req, width: uiWidth(columns), showDescriptions: budget.showDescriptions, onResolveV1: (result) => {
|
|
627
|
+
slot = (_jsx(AskCard, { request: req, width: uiWidth(columns), showDescriptions: budget.showDescriptions, maxRows: askMaxRows, onResolveV1: (result) => {
|
|
618
628
|
if (!('questions' in req))
|
|
619
629
|
req.resolve(result);
|
|
620
630
|
clearAskIfCurrent(req);
|
|
@@ -16,6 +16,35 @@ import { verbLine, viewAffordance, toSourceLines } from '../approvals/op-labels.
|
|
|
16
16
|
import { useReclaimRawStdin } from './widgets/reclaim-raw-stdin.js';
|
|
17
17
|
import { getLook } from '../renderer/look.js';
|
|
18
18
|
import { roleProps, inkChip, legacyInkColor } from './role-props.js';
|
|
19
|
+
import { wrapRows } from '../renderer/wrap-rows.js';
|
|
20
|
+
import { uiWidth } from '../renderer/ui-width.js';
|
|
21
|
+
/**
|
|
22
|
+
* Fix batch 2026-10-12 (Task 2): why the risk went up is the decision input,
|
|
23
|
+
* so the "Risk: … (raised: …)" text wraps onto up to this many rows (both
|
|
24
|
+
* looks) instead of being cut. The verb and meta rows stay one row (chrome).
|
|
25
|
+
*/
|
|
26
|
+
export const RISK_MAX_ROWS = 4;
|
|
27
|
+
/** Border 2 + paddingX 2 on each side. */
|
|
28
|
+
const MODAL_CHROME_COLS = 6;
|
|
29
|
+
function riskText(effectiveRisk) {
|
|
30
|
+
const raised = shownRiskReasons(effectiveRisk.raisedBy).map(riskReasonText).join('; ');
|
|
31
|
+
return `Risk: ${effectiveRisk.level}${raised ? ` (raised: ${raised})` : ''}`;
|
|
32
|
+
}
|
|
33
|
+
/** The risk text as the rows it is drawn on at card width `width`. */
|
|
34
|
+
export function riskRows(effectiveRisk, width) {
|
|
35
|
+
return wrapRows(riskText(effectiveRisk), Math.max(1, width - MODAL_CHROME_COLS), RISK_MAX_ROWS);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The collapsed card's rendered height at `width` (the live-area budget; the
|
|
39
|
+
* App adds its own one-row top margin). Panels: border 2 + paddingY 2 + verb
|
|
40
|
+
* + meta? + risk rows + gap + keys. Legacy adds the "Write approval" title and
|
|
41
|
+
* the gap under it.
|
|
42
|
+
*/
|
|
43
|
+
export function estimateApprovalModalRows(p, width = uiWidth()) {
|
|
44
|
+
const meta = p.change.package || p.transport ? 1 : 0;
|
|
45
|
+
const body = 1 + meta + riskRows(p.effectiveRisk, width).length + 2;
|
|
46
|
+
return 4 + body + (getLook() === 'panels' ? 0 : 2);
|
|
47
|
+
}
|
|
19
48
|
// Legacy look: today's literal names (bytes unchanged while colour is on).
|
|
20
49
|
function riskColor(level) {
|
|
21
50
|
return level === 'high' ? 'red' : level === 'medium' ? 'yellow' : 'green';
|
|
@@ -35,7 +64,8 @@ function diffLineColor(line, panels) {
|
|
|
35
64
|
export const BACK_GUARD_MS = 300;
|
|
36
65
|
/** The review view's key line, always at its bottom: how to go back and how to answer from here. */
|
|
37
66
|
export const viewKeysLine = (reviewKey) => `Esc or ${reviewKey}: back · a: approve · s: skip · c: cancel turn`;
|
|
38
|
-
export function ApprovalModal({ change, effectiveRisk, transport, onApply, onSkip, onDiff, onCancel, onInterruptTurn }) {
|
|
67
|
+
export function ApprovalModal({ change, effectiveRisk, transport, onApply, onSkip, onDiff, onCancel, onInterruptTurn, width }) {
|
|
68
|
+
const cardWidth = width ?? uiWidth();
|
|
39
69
|
// BUG-1 — re-assert raw mode on mount so keypresses reach useInput even
|
|
40
70
|
// when an inquirer gate ran earlier in the turn (see import block).
|
|
41
71
|
useReclaimRawStdin();
|
|
@@ -148,7 +178,8 @@ export function ApprovalModal({ change, effectiveRisk, transport, onApply, onSki
|
|
|
148
178
|
: '';
|
|
149
179
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: panels ? roleProps('frame').color : quiet, paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [isDiff ? 'Diff' : aff.label, _jsx(Text, { color: quiet, children: where })] }), change.diff ? (isDiff ? (shownLines.map((line, i) => (_jsx(Text, { wrap: "truncate-end", color: diffLineColor(line, panels), children: line }, from + i)))) : (shownLines.map((line, i) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: quiet, children: [String(from + i + 1).padStart(4), " "] }), line] }, from + i))))) : (_jsx(Text, { dimColor: true, children: "(no source provided by the model)" })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { wrap: "truncate-end", children: viewKeysLine(aff.key) }) })] }));
|
|
150
180
|
}
|
|
151
|
-
|
|
181
|
+
// Pre-wrapped rows, never Ink soft-wrap (Wave-1 rule): every row is counted.
|
|
182
|
+
const risks = riskRows(effectiveRisk, cardWidth);
|
|
152
183
|
// Meta line: Package · Transport — each segment only when genuinely known.
|
|
153
184
|
// The handler resolves `transport` per op (see op-labels displayTransport:
|
|
154
185
|
// create-family may fall back to the session transport because the create
|
|
@@ -170,11 +201,12 @@ export function ApprovalModal({ change, effectiveRisk, transport, onApply, onSki
|
|
|
170
201
|
keys.push('[s] Skip', '[c] Cancel turn');
|
|
171
202
|
// Panels look — same content, keys and risk colour; restyled only. The
|
|
172
203
|
// approval runs BEFORE snapshot/write/syntax check, so the card claims no
|
|
173
|
-
// step has run.
|
|
204
|
+
// step has run. (Fix batch 2026-10-12: the owner lifted the legacy byte pin
|
|
205
|
+
// for the wrapped risk rows and the fixed card width.)
|
|
174
206
|
if (getLook() === 'panels') {
|
|
175
207
|
const risk = roleProps(riskRole(effectiveRisk.level));
|
|
176
208
|
const tag = inkChip('APPROVAL');
|
|
177
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: risk.color, paddingX: 2, paddingY: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...tag.props, children: tag.text }), _jsx(Text, { children: ' ' }), _jsx(Text, { ...(change.op === 'delete' ? roleProps('bad') : {}), bold: true, children: verb }), change.op === 'create' ? _jsx(Text, { ...roleProps('dim'), children: " (new object)" }) : null] }), metaSegs.length > 0 && _jsx(Text, { ...roleProps('dim'), wrap: "truncate-end", children: metaSegs.join(' · ') }),
|
|
209
|
+
return (_jsxs(Box, { flexDirection: "column", width: cardWidth, borderStyle: "round", borderColor: risk.color, paddingX: 2, paddingY: 1, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...tag.props, children: tag.text }), _jsx(Text, { children: ' ' }), _jsx(Text, { ...(change.op === 'delete' ? roleProps('bad') : {}), bold: true, children: verb }), change.op === 'create' ? _jsx(Text, { ...roleProps('dim'), children: " (new object)" }) : null] }), metaSegs.length > 0 && _jsx(Text, { ...roleProps('dim'), wrap: "truncate-end", children: metaSegs.join(' · ') }), risks.map((row, i) => _jsx(Text, { ...risk, wrap: "truncate-end", children: row }, `risk-${i}`)), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...roleProps('sap', { bold: true }), children: keys[0] }), _jsx(Text, { children: ' ' + keys.slice(1).join(' ') })] }) })] }));
|
|
178
210
|
}
|
|
179
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: legacyInkColor(riskColor(effectiveRisk.level)), paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Write approval" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: verbColor, children: verb }), change.op === 'create' ? _jsx(Text, { dimColor: true, children: " (new object)" }) : null] }) }), metaSegs.length > 0 && (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: metaSegs.join(' · ') })),
|
|
211
|
+
return (_jsxs(Box, { flexDirection: "column", width: cardWidth, borderStyle: "round", borderColor: legacyInkColor(riskColor(effectiveRisk.level)), paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Write approval" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: verbColor, children: verb }), change.op === 'create' ? _jsx(Text, { dimColor: true, children: " (new object)" }) : null] }) }), metaSegs.length > 0 && (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: metaSegs.join(' · ') })), risks.map((row, i) => (_jsx(Text, { color: legacyInkColor(riskColor(effectiveRisk.level)), wrap: "truncate-end", children: row }, `risk-${i}`))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { wrap: "truncate-end", children: keys.join(' ') }) })] }));
|
|
180
212
|
}
|
package/dist/ui/live-area.js
CHANGED
|
@@ -23,7 +23,8 @@ export const INPUT_MAX_LINES = 6;
|
|
|
23
23
|
* occupant, fed to liveBudget as `cardRows`. They cover the collapsed card.
|
|
24
24
|
* A task that owns a card with a variable height replaces its entry at the
|
|
25
25
|
* App call site with a real estimator (Q2: estimateAskCardRows, C1:
|
|
26
|
-
* estimateConfirmCardRows
|
|
26
|
+
* estimateConfirmCardRows, fix batch 2026-10-12: estimateApprovalModalRows
|
|
27
|
+
* and estimateCoachingPickerRows).
|
|
27
28
|
* approval margin 1 + border 2 + padding 2 + 3 fact rows + margin 1 + keys 1
|
|
28
29
|
* rewind margin 1 + border 2 + title 1 + margin 1 + more 2 + REWIND_VISIBLE_ROWS 9 + margin 1 + hint 1
|
|
29
30
|
* timeline margin 1 + border 2 + title 1 + margin 1 + more 2 + TIMELINE_VISIBLE_ROWS 9
|
|
@@ -69,3 +70,15 @@ export function liveBudget(args) {
|
|
|
69
70
|
const inputMaxLines = Math.max(1, Math.min(Math.max(1, args.inputLines), INPUT_MAX_LINES, remaining));
|
|
70
71
|
return { checklistRows, inputMaxLines, showDescriptions: remaining >= 1 };
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Fix round 1 (review I1): the rows a card may take so the whole live area
|
|
75
|
+
* stays ≤ rows − 2 with one input line beside it — the cap less the status
|
|
76
|
+
* strip, the input border, the footer, an open palette, the checklist's one
|
|
77
|
+
* folded line and one input line. A card that can fold (AskCard) folds to
|
|
78
|
+
* fit this.
|
|
79
|
+
*/
|
|
80
|
+
export function cardRowBudget(args) {
|
|
81
|
+
const cap = Math.max(0, args.rows - 2);
|
|
82
|
+
const fixed = STATUS_ROWS + INPUT_BORDER_ROWS + FOOTER_ROWS + Math.max(0, args.paletteRows);
|
|
83
|
+
return Math.max(1, cap - fixed - 1 - 1);
|
|
84
|
+
}
|