@cspeach/cli 1.1.0 → 1.1.2
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/loop.js +11 -0
- package/dist/approvals/jwt.js +12 -1
- package/dist/cost/session-cost.js +28 -0
- package/dist/renderer/abap-inline.js +2 -1
- package/dist/renderer/syntax.js +7 -1
- package/dist/repl/current-transport.js +23 -1
- package/dist/repl/post-turn-status.js +9 -6
- package/dist/repl.js +4 -0
- package/dist/skills/bundled-skills.js +57 -57
- package/dist/tools/fiori/fe-scaffold.js +25 -0
- package/dist/tools/fiori/smoke/assertions.js +8 -2
- package/dist/tools/fiori/smoke/driver.js +6 -0
- package/dist/tools/fiori/smoke/run-smoke.js +69 -2
- package/dist/tools/fiori/tools.js +4 -2
- package/dist/tools/sap-write.js +18 -0
- package/dist/tools/todo.js +11 -3
- package/dist/ui/app.js +1 -1
- package/dist/ui/sap-state-store.js +1 -1
- package/dist/ui/status-line.js +2 -2
- package/dist/ui/todo-panel.js +27 -6
- package/fiori/zlas-inspection-lr/package.json +26 -0
- package/fiori/zlas-inspection-lr/ui5-local.yaml +45 -0
- package/fiori/zlas-inspection-lr/ui5.yaml +41 -0
- package/fiori/zlas-inspection-lr/webapp/Component.js +12 -0
- package/fiori/zlas-inspection-lr/webapp/i18n/i18n.properties +9 -0
- package/fiori/zlas-inspection-lr/webapp/index.html +35 -0
- package/fiori/zlas-inspection-lr/webapp/manifest.json +145 -0
- package/package.json +2 -2
|
@@ -62,6 +62,31 @@ export async function scaffoldFioriElements(opts) {
|
|
|
62
62
|
ui5: { minUI5Version: ui5Version, version: ui5Version },
|
|
63
63
|
};
|
|
64
64
|
await generate(opts.basePath, config, fs);
|
|
65
|
+
if (!opts.localAnnotations) {
|
|
66
|
+
// @sap-ux writers force-inject a 'localAnnotationsName: annotation' dataSource for
|
|
67
|
+
// EDMX services even when none was requested, but only write the XML file when
|
|
68
|
+
// metadata is passed — leaving a manifest reference that 404s at boot (F-L2-7).
|
|
69
|
+
const manifestPath = join(opts.basePath, 'webapp', 'manifest.json');
|
|
70
|
+
const manifest = fs.readJSON(manifestPath);
|
|
71
|
+
const ds = manifest?.['sap.app']?.dataSources ?? {};
|
|
72
|
+
const removed = Object.keys(ds).filter((k) => ds[k]?.type === 'ODataAnnotation');
|
|
73
|
+
if (removed.length > 0) {
|
|
74
|
+
for (const k of removed)
|
|
75
|
+
delete ds[k];
|
|
76
|
+
for (const d of Object.values(ds)) {
|
|
77
|
+
if (Array.isArray(d?.settings?.annotations)) {
|
|
78
|
+
d.settings.annotations = d.settings.annotations.filter((name) => !removed.includes(name));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
fs.writeJSON(manifestPath, manifest);
|
|
82
|
+
// also drop the orphan empty EDMX if the writer emitted one (metadata path)
|
|
83
|
+
for (const k of removed) {
|
|
84
|
+
const orphan = join(opts.basePath, 'webapp', 'annotations', `${k}.xml`);
|
|
85
|
+
if (fs.exists(orphan))
|
|
86
|
+
fs.delete(orphan);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
65
90
|
// Overwrite the writer's empty EDMX skeleton with the caller's real annotations
|
|
66
91
|
// EDMX, in the mem-fs editor, BEFORE commit so the flushed file is the caller's.
|
|
67
92
|
if (opts.localAnnotations) {
|
|
@@ -3,15 +3,21 @@
|
|
|
3
3
|
// under the gateway/odata4 path. An incidental non-OData 404 (favicon, a
|
|
4
4
|
// analytics beacon) must NOT false-fail an otherwise healthy render.
|
|
5
5
|
const ODATA_URL = /\$metadata|\$batch|\/sap\/opu\/odata|\/odata4?\//i;
|
|
6
|
+
// Unbuilt dev-serve artifacts: `fiori run` 404s Component-preload.js and
|
|
7
|
+
// resources.json on every app that hasn't been through `ui5 build`.
|
|
8
|
+
// Dev-mode noise, not app defects (F-L2-12) — must not fail boot.
|
|
9
|
+
const UNBUILT_DEV_404 = /(Component-preload\.js|resources\.json)/i;
|
|
10
|
+
const LOAD_FAILURE = /(404|failed to load)/i;
|
|
6
11
|
/** Boot check: the app loaded without throwing anything to the console. */
|
|
7
12
|
export function checkBoot(consoleErrors) {
|
|
8
|
-
const
|
|
13
|
+
const real = consoleErrors.filter((e) => !(UNBUILT_DEV_404.test(e) && LOAD_FAILURE.test(e)));
|
|
14
|
+
const ok = real.length === 0;
|
|
9
15
|
return {
|
|
10
16
|
name: 'boot',
|
|
11
17
|
ok,
|
|
12
18
|
detail: ok
|
|
13
19
|
? 'app booted with no console errors'
|
|
14
|
-
: `${
|
|
20
|
+
: `${real.length} console error(s) during boot: ${real.slice(0, 3).join(' | ')}`,
|
|
15
21
|
};
|
|
16
22
|
}
|
|
17
23
|
/** OData check: no 4xx/5xx on any $metadata / $batch / entity-set request. */
|
|
@@ -74,6 +74,12 @@ export async function openApp(executablePath, url, timeoutMs) {
|
|
|
74
74
|
}, name);
|
|
75
75
|
await page.waitForNetworkIdle({ idleTime: 200, timeout: timeoutMs }).catch(() => { });
|
|
76
76
|
},
|
|
77
|
+
// F-L2-10A: evidence capture. The caller owns the artifact dir (it is
|
|
78
|
+
// created before this is called) and owns failure handling — a rejection
|
|
79
|
+
// here is caught there and the entry omitted, never failing the smoke.
|
|
80
|
+
screenshot: async (target) => {
|
|
81
|
+
await page.screenshot({ path: target });
|
|
82
|
+
},
|
|
77
83
|
close: async () => {
|
|
78
84
|
await browser.close();
|
|
79
85
|
},
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
//
|
|
19
19
|
// The browser + driver are injectable (RunSmokeDeps) so the unit suite drives
|
|
20
20
|
// every path with fakes and NEVER launches a real browser.
|
|
21
|
+
import { mkdirSync } from 'node:fs';
|
|
22
|
+
import * as os from 'node:os';
|
|
23
|
+
import * as path from 'node:path';
|
|
21
24
|
import { resolveBrowserExecutable } from './browser.js';
|
|
22
25
|
import { openApp as realOpenApp } from './driver.js';
|
|
23
26
|
import { checkBoot, checkOData, checkColumns, checkRows, checkExercise, } from './assertions.js';
|
|
@@ -44,6 +47,30 @@ export const SMOKE_SELECTORS = {
|
|
|
44
47
|
// us to an auth endpoint instead of serving the app.
|
|
45
48
|
const LOGON_URL = /sap-system-login|\/saml2\/|\/logon|\/login/i;
|
|
46
49
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
50
|
+
/**
|
|
51
|
+
* A filesystem-safe app name for the artifact dir, derived from the preview URL:
|
|
52
|
+
* the last real path segment (`…/mycourses/index.html` → `mycourses`), falling
|
|
53
|
+
* back to host+port (`http://localhost:8080/index.html` → `localhost-8080`).
|
|
54
|
+
*/
|
|
55
|
+
function appNameFromUrl(appUrl) {
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
const u = new URL(appUrl);
|
|
59
|
+
const segs = u.pathname.split('/').filter((s) => s.length > 0 && !s.includes('.'));
|
|
60
|
+
raw = segs.length > 0 ? segs[segs.length - 1] : u.host;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
raw = appUrl;
|
|
64
|
+
}
|
|
65
|
+
const slug = raw.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
66
|
+
return slug.length > 0 ? slug : 'app';
|
|
67
|
+
}
|
|
68
|
+
/** Local-time `yyyyMMdd-HHmmss` stamp — one dir per smoke run, sorts naturally. */
|
|
69
|
+
function runStamp(now = new Date()) {
|
|
70
|
+
const p2 = (n) => String(n).padStart(2, '0');
|
|
71
|
+
return (`${now.getFullYear()}${p2(now.getMonth() + 1)}${p2(now.getDate())}` +
|
|
72
|
+
`-${p2(now.getHours())}${p2(now.getMinutes())}${p2(now.getSeconds())}`);
|
|
73
|
+
}
|
|
47
74
|
/** A not-run result: manual verification, a single explanatory skipped check. */
|
|
48
75
|
function skipped(detail) {
|
|
49
76
|
return { verification: 'manual', passed: false, checks: [{ name: 'skipped', ok: false, detail }] };
|
|
@@ -111,6 +138,31 @@ export async function runSmoke(spec, deps) {
|
|
|
111
138
|
catch {
|
|
112
139
|
return skipped(`preview server not reachable at ${spec.appUrl}; run npm run start first`);
|
|
113
140
|
}
|
|
141
|
+
// F-L2-10A evidence capture. The dir is created LAZILY on the first successful
|
|
142
|
+
// capture, so a skip path (or a driver with no screenshot capability) writes
|
|
143
|
+
// nothing at all. Every capture is try/catch-wrapped: a screenshot failure
|
|
144
|
+
// omits the entry and NEVER fails the smoke.
|
|
145
|
+
const screenshots = {};
|
|
146
|
+
const canShoot = typeof page.screenshot === 'function';
|
|
147
|
+
let shotDir;
|
|
148
|
+
const capture = async (key, file) => {
|
|
149
|
+
if (!page.screenshot)
|
|
150
|
+
return;
|
|
151
|
+
try {
|
|
152
|
+
shotDir ??=
|
|
153
|
+
deps.artifactDir ??
|
|
154
|
+
path.join(os.homedir(), '.cspeach', 'smoke', `${appNameFromUrl(spec.appUrl)}-${runStamp()}`);
|
|
155
|
+
mkdirSync(shotDir, { recursive: true });
|
|
156
|
+
const target = path.join(shotDir, file);
|
|
157
|
+
await page.screenshot(target);
|
|
158
|
+
screenshots[key] = target;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Evidence is best-effort — a capture problem must never fail the smoke.
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
// Hoisted so the finally block can decide whether a failure shot is owed.
|
|
165
|
+
const checks = [];
|
|
114
166
|
try {
|
|
115
167
|
// Skip path 4 — SAP logon page (deployed BSP / unauthenticated preview).
|
|
116
168
|
// Detect via a logon-redirect URL OR the presence of a logon-form field, and
|
|
@@ -120,9 +172,10 @@ export async function runSmoke(spec, deps) {
|
|
|
120
172
|
if (LOGON_URL.test(url) || logonHits.length > 0) {
|
|
121
173
|
return skipped(`preview served a SAP logon page (not authenticated) at ${url}; run npm run start first`);
|
|
122
174
|
}
|
|
175
|
+
// The app is open past the logon check — this is the boot evidence shot.
|
|
176
|
+
await capture('boot', 'boot.png');
|
|
123
177
|
// Real render — assert content. boot + odata always; columns only when the
|
|
124
178
|
// spec asked for them; rows only for a rows-bearing (table) control.
|
|
125
|
-
const checks = [];
|
|
126
179
|
checks.push(checkBoot(page.consoleErrors()));
|
|
127
180
|
checks.push(checkOData(page.failedRequests()));
|
|
128
181
|
if (spec.expectedColumns && spec.expectedColumns.length > 0) {
|
|
@@ -141,9 +194,23 @@ export async function runSmoke(spec, deps) {
|
|
|
141
194
|
const results = await runExercises(page, spec.exercises);
|
|
142
195
|
checks.push(checkExercise(results));
|
|
143
196
|
}
|
|
144
|
-
|
|
197
|
+
// Post-exercise evidence: the app's final state after every declared
|
|
198
|
+
// interaction ran (and simply the settled app when the spec declares none).
|
|
199
|
+
await capture('postExercise', 'post-exercise.png');
|
|
200
|
+
// `screenshots` is the SAME object the finally block may still add the
|
|
201
|
+
// failure entry to, so a failing run carries it even though the value is
|
|
202
|
+
// built here. It rides along only when the driver can shoot at all.
|
|
203
|
+
return {
|
|
204
|
+
verification: 'smoke',
|
|
205
|
+
passed: checks.every((c) => c.ok),
|
|
206
|
+
checks,
|
|
207
|
+
...(canShoot ? { screenshots } : {}),
|
|
208
|
+
};
|
|
145
209
|
}
|
|
146
210
|
finally {
|
|
211
|
+
// A failing check earns a failure shot — captured BEFORE the page is closed.
|
|
212
|
+
if (checks.some((c) => !c.ok))
|
|
213
|
+
await capture('failure', 'failure.png');
|
|
147
214
|
await page.close();
|
|
148
215
|
}
|
|
149
216
|
}
|
|
@@ -595,8 +595,10 @@ registerTool({
|
|
|
595
595
|
'derived one. Derivation notes surface as a `warnings` array in the result. SKIPPABLE BUT NEVER ' +
|
|
596
596
|
'SILENTLY GREEN: when the render_smoke config is off, no browser is found, the preview is unreachable, ' +
|
|
597
597
|
'or the page is a SAP logon form, it returns verification:"manual" + passed:false with a "skipped" ' +
|
|
598
|
-
'check explaining why — it can never report a passing smoke without a real render.
|
|
599
|
-
'
|
|
598
|
+
'check explaining why — it can never report a passing smoke without a real render. On a real render it ' +
|
|
599
|
+
'also saves boot / post-exercise / failure screenshots under ~/.cspeach/smoke/ and returns their absolute ' +
|
|
600
|
+
'paths in a `screenshots` object (best-effort evidence — a capture failure never fails the smoke). ' +
|
|
601
|
+
'Flag-gated (enabled with local_build), read-only.',
|
|
600
602
|
isMutating: false,
|
|
601
603
|
category: 'fiori',
|
|
602
604
|
flagGated: true,
|
package/dist/tools/sap-write.js
CHANGED
|
@@ -32,6 +32,10 @@ import { auditLog } from '@cspeach/sap-client';
|
|
|
32
32
|
import { ERR } from '../errors/codes.js';
|
|
33
33
|
import { interceptUpdateMethod, CancelledByUserError, } from './update-method-intercept.js';
|
|
34
34
|
import { resolveWriteTransport } from './transport-resolution.js';
|
|
35
|
+
// F-L2-1: statusline-only wire — records the transport a write ACTUALLY
|
|
36
|
+
// stamped so the `tr` segment stops reading `none` for plan-supplied
|
|
37
|
+
// transports. Never touches the session transport pointer.
|
|
38
|
+
import { noteWriteTransport } from '../repl/current-transport.js';
|
|
35
39
|
import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
|
|
36
40
|
import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
|
|
37
41
|
// ── Transcript visibility helpers (D16-display + tour slide-7, 2026-06-12) ──
|
|
@@ -208,6 +212,7 @@ registerTool({
|
|
|
208
212
|
};
|
|
209
213
|
}
|
|
210
214
|
await auditLog('setSource', args.type, args.name, 'success', Date.now() - startedAt);
|
|
215
|
+
noteWriteTransport(resolved.transport); // F-L2-1 statusline wire
|
|
211
216
|
// ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
|
|
212
217
|
const syntax = await verifySyntax(ctx.adt, args.name, args.type);
|
|
213
218
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ written: true, syntax }), false, Date.now() - startedAt);
|
|
@@ -400,6 +405,7 @@ registerTool({
|
|
|
400
405
|
};
|
|
401
406
|
}
|
|
402
407
|
await auditLog('updateMethod', 'CLAS', args.class_name, 'success', Date.now() - startedAt, `method=${args.method_name}`);
|
|
408
|
+
noteWriteTransport(resolved.transport); // F-L2-1 statusline wire
|
|
403
409
|
// ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
|
|
404
410
|
const syntax = await verifySyntax(ctx.adt, args.class_name, 'CLAS');
|
|
405
411
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ updated: true, syntax }), false, Date.now() - startedAt);
|
|
@@ -530,6 +536,7 @@ registerTool({
|
|
|
530
536
|
};
|
|
531
537
|
}
|
|
532
538
|
await auditLog('deleteObject', args.type, args.name, 'success', Date.now() - startedAt);
|
|
539
|
+
noteWriteTransport(resolved.transport); // F-L2-1 statusline wire
|
|
533
540
|
// ── Post-check: object should no longer exist ────────────────────────────
|
|
534
541
|
const stillExists = await ctx.adt.objectStructure(args.type, args.name)
|
|
535
542
|
.then(() => true)
|
|
@@ -630,6 +637,9 @@ registerTool({
|
|
|
630
637
|
is_error: true,
|
|
631
638
|
};
|
|
632
639
|
}
|
|
640
|
+
// F-L2-1 statusline wire. This path never reaches resolveWriteTransport —
|
|
641
|
+
// the 3-tier effectiveTransport above is what SAP stamped.
|
|
642
|
+
noteWriteTransport(effectiveTransport);
|
|
633
643
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), false, Date.now() - startedAt);
|
|
634
644
|
return {
|
|
635
645
|
content: JSON.stringify({
|
|
@@ -752,6 +762,7 @@ registerTool({
|
|
|
752
762
|
};
|
|
753
763
|
}
|
|
754
764
|
await auditLog('setClassInclude', 'CLAS', args.className, 'success', Date.now() - startedAt, `include=${args.includeType}`);
|
|
765
|
+
noteWriteTransport(resolved.transport); // F-L2-1 statusline wire
|
|
755
766
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ written: true, variant: result.variantUsed }), false, Date.now() - startedAt);
|
|
756
767
|
// Tour slide-7 chain. Honest states: includes have no standalone syntax
|
|
757
768
|
// gate (checked at activation), so ③ renders as deferred, never faked.
|
|
@@ -879,6 +890,11 @@ registerTool({
|
|
|
879
890
|
is_error: true,
|
|
880
891
|
};
|
|
881
892
|
}
|
|
893
|
+
// F-L2-1 statusline wire. Recorded on CREATED, not on active: the object is
|
|
894
|
+
// already in effectiveTransport by then, so a create-succeeded /
|
|
895
|
+
// activation-failed run must still show which request holds it. DDIC creates
|
|
896
|
+
// bypass resolveWriteTransport — effectiveTransport is what SAP stamped.
|
|
897
|
+
noteWriteTransport(effectiveTransport);
|
|
882
898
|
if (!result.active) {
|
|
883
899
|
// Rule 10: created but did not activate — surface as an error.
|
|
884
900
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
@@ -1002,6 +1018,8 @@ registerTool({
|
|
|
1002
1018
|
is_error: true,
|
|
1003
1019
|
};
|
|
1004
1020
|
}
|
|
1021
|
+
// F-L2-1 statusline wire — see the createDomain note above (created, not active).
|
|
1022
|
+
noteWriteTransport(effectiveTransport);
|
|
1005
1023
|
if (!result.active) {
|
|
1006
1024
|
// Rule 10: created but did not activate — surface as an error.
|
|
1007
1025
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
package/dist/tools/todo.js
CHANGED
|
@@ -97,7 +97,11 @@ export async function todoSetHandler(args, ctx) {
|
|
|
97
97
|
ctx.chunkEmitter?.emit('chunk', formatTodoChecklistBlock(todos));
|
|
98
98
|
const result = {
|
|
99
99
|
count: todos.length,
|
|
100
|
-
|
|
100
|
+
// LAST-wins, matching the app strip and Ctrl+T panel (both `.at(-1)`).
|
|
101
|
+
// With a batched transition (several items flipped in one call) first-wins
|
|
102
|
+
// would tell the model step 2 is running while the user's ▶ points at
|
|
103
|
+
// step 5 — the echo and the UI must name the same item.
|
|
104
|
+
in_progress: inProgress.at(-1)?.text ?? null,
|
|
101
105
|
};
|
|
102
106
|
if (inProgress.length > 1) {
|
|
103
107
|
result.warning =
|
|
@@ -108,10 +112,14 @@ export async function todoSetHandler(args, ctx) {
|
|
|
108
112
|
}
|
|
109
113
|
registerTool({
|
|
110
114
|
name: 'todo_set',
|
|
111
|
-
// Model-behavior lever — this exact framing is binding (task-4 brief
|
|
115
|
+
// Model-behavior lever — this exact framing is binding (task-4 brief;
|
|
116
|
+
// the no-batching nudge is binding per the F-L2-3 task-5 brief).
|
|
112
117
|
description: 'Maintain your task list for multi-step work. Call with the FULL updated list whenever you ' +
|
|
113
118
|
'complete a step, start a new one, or discover new work. Exactly one item should be ' +
|
|
114
|
-
'in_progress at a time. The user sees this list — keep items short and outcome-shaped.'
|
|
119
|
+
'in_progress at a time. The user sees this list — keep items short and outcome-shaped. ' +
|
|
120
|
+
'Update at EVERY step transition: when you finish a step, immediately call todo_set ' +
|
|
121
|
+
'marking it completed and the next step in_progress. Never batch several finished steps ' +
|
|
122
|
+
'into one later call — the user watches the ▶ indicator live.',
|
|
115
123
|
isMutating: false,
|
|
116
124
|
category: 'session',
|
|
117
125
|
input_schema: {
|
package/dist/ui/app.js
CHANGED
|
@@ -510,7 +510,7 @@ export function App({ chunkEmitter, onSubmit, onSlashTrigger, onHistoryTrigger,
|
|
|
510
510
|
askQuestionPendingRef.current = null;
|
|
511
511
|
setAskQuestionPending(null);
|
|
512
512
|
}
|
|
513
|
-
} }, askQuestionSeq))), _jsx(TurnStatus, { inProgressTodo: todos.
|
|
513
|
+
} }, askQuestionSeq))), _jsx(TurnStatus, { inProgressTodo: todos.filter((t) => t.status === 'in_progress').at(-1)?.text ?? null }), _jsx(StatusLine, {}), _jsx(Footer, { onSubmit: beginSubmit, onSlashTrigger: handleSlashTrigger, onHistoryTrigger: handleHistoryTrigger, prefill: prefill, onValueChange: setFooterValue, onBufferChange: setFooterBufferCount,
|
|
514
514
|
// Task 7 — multiline announcements (draft discarded) land as
|
|
515
515
|
// dim scrollback lines, house style for Footer-adjacent notices.
|
|
516
516
|
onNotice: (t) => chunkEmitter.emit('chunk', `\x1b[2m${t}\x1b[22m\n`),
|
|
@@ -14,7 +14,7 @@ import React, { createContext, useContext } from 'react';
|
|
|
14
14
|
const DEFAULT_STATE = {
|
|
15
15
|
system: '', user: '', transport: null, locksHeld: [],
|
|
16
16
|
tokenUsageToday: 0, dollarSpendToday: 0, dollarCap: 100, recentTurns: [],
|
|
17
|
-
writeMode: null,
|
|
17
|
+
writeMode: null, lastWriteTransport: null,
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
20
|
* Rate limiter: calls `fn` at most once every `minIntervalMs`. Trailing-only:
|
package/dist/ui/status-line.js
CHANGED
|
@@ -37,7 +37,7 @@ export function StatusLine() {
|
|
|
37
37
|
// the field lengths.
|
|
38
38
|
return (_jsxs(Text, { wrap: "truncate-end", children: [s.system
|
|
39
39
|
? _jsx(Text, { color: "cyan", children: s.system })
|
|
40
|
-
: _jsx(Text, { dimColor: true, children: "no system" }), _jsx(Text, { dimColor: true, children: " \u2502 " }), _jsx(Text, { color: "white", children: s.user || '—' }), _jsx(Text, { dimColor: true, children: " \u2502 tr " }), s.transport
|
|
41
|
-
? _jsx(Text, { color: "yellow", children: s.transport })
|
|
40
|
+
: _jsx(Text, { dimColor: true, children: "no system" }), _jsx(Text, { dimColor: true, children: " \u2502 " }), _jsx(Text, { color: "white", children: s.user || '—' }), _jsx(Text, { dimColor: true, children: " \u2502 tr " }), (s.transport ?? s.lastWriteTransport)
|
|
41
|
+
? _jsx(Text, { color: "yellow", children: s.transport ?? s.lastWriteTransport })
|
|
42
42
|
: _jsx(Text, { dimColor: true, children: "none" }), s.writeMode && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " \u2502 " }), _jsx(Text, { color: MODE_COLORS[s.writeMode], children: s.writeMode })] })), _jsx(Text, { dimColor: true, children: " \u2502 " }), _jsx(Text, { color: "green", children: spendDisplay })] }));
|
|
43
43
|
}
|
package/dist/ui/todo-panel.js
CHANGED
|
@@ -16,8 +16,10 @@ import { Box, Text, useInput } from 'ink';
|
|
|
16
16
|
*
|
|
17
17
|
* Lists longer than TODO_VISIBLE_ROWS get a scrolling window
|
|
18
18
|
* (CommandPalette pattern): ↑/↓ move the window, hidden counts show as
|
|
19
|
-
* "↑ N more" / "↓ N more". The initial window anchors the in_progress
|
|
19
|
+
* "↑ N more" / "↓ N more". The initial window anchors the live in_progress
|
|
20
20
|
* item ~3 rows from the top so the current step is visible on open.
|
|
21
|
+
* "Live" = the LAST in_progress item in list order (F-L2-3) — see
|
|
22
|
+
* activeTodoIndex below.
|
|
21
23
|
*/
|
|
22
24
|
const PEACH = '#FFCBA4';
|
|
23
25
|
export const TODO_VISIBLE_ROWS = 12;
|
|
@@ -28,15 +30,31 @@ export function formatTodoCounts(todos) {
|
|
|
28
30
|
const open = todos.filter((t) => t.status === 'pending').length;
|
|
29
31
|
return `Tasks — ${done} done · ${active} active · ${open} open`;
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Index of the LIVE step — the LAST in_progress item in list order, or -1.
|
|
35
|
+
*
|
|
36
|
+
* F-L2-3 (sitting #1): todo_set tolerates several in_progress items (a
|
|
37
|
+
* warning, not an error — todo.ts), and models routinely batch transitions,
|
|
38
|
+
* leaving an earlier item still marked in_progress. List order IS plan
|
|
39
|
+
* order, so the later item is the one actually being worked; picking the
|
|
40
|
+
* FIRST match pinned the ▶ on a step already long finished.
|
|
41
|
+
*/
|
|
42
|
+
export function activeTodoIndex(todos) {
|
|
43
|
+
for (let i = todos.length - 1; i >= 0; i--) {
|
|
44
|
+
if (todos[i].status === 'in_progress')
|
|
45
|
+
return i;
|
|
46
|
+
}
|
|
47
|
+
return -1;
|
|
48
|
+
}
|
|
31
49
|
export function TodoPanel({ todos, onClose }) {
|
|
32
50
|
const maxOffset = Math.max(0, todos.length - TODO_VISIBLE_ROWS);
|
|
33
|
-
|
|
51
|
+
const activeIdx = activeTodoIndex(todos);
|
|
52
|
+
// Anchor the initial window so the live in_progress item sits ~3 rows from
|
|
34
53
|
// the top (CommandPalette's cursor rule, with the active item as cursor).
|
|
35
54
|
const [offset, setOffset] = useState(() => {
|
|
36
|
-
|
|
37
|
-
if (idx <= 3)
|
|
55
|
+
if (activeIdx <= 3)
|
|
38
56
|
return 0;
|
|
39
|
-
return Math.min(
|
|
57
|
+
return Math.min(activeIdx - 3, maxOffset);
|
|
40
58
|
});
|
|
41
59
|
useInput((input, key) => {
|
|
42
60
|
if (key.escape || input === 'q' || (key.ctrl && (input === 't' || input === 'T'))) {
|
|
@@ -56,7 +74,10 @@ export function TodoPanel({ todos, onClose }) {
|
|
|
56
74
|
if (t.status === 'completed') {
|
|
57
75
|
return _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u25A3 ", t.text] }, key);
|
|
58
76
|
}
|
|
59
|
-
|
|
77
|
+
// Only the LIVE step gets the ▶; a stale earlier in_progress item
|
|
78
|
+
// (batched transition) renders as an ordinary open row, so the
|
|
79
|
+
// panel never shows two arrows or points at finished work.
|
|
80
|
+
if (t.status === 'in_progress' && clamped + i === activeIdx) {
|
|
60
81
|
return _jsxs(Text, { color: PEACH, bold: true, wrap: "truncate-end", children: ["\u25B6 ", t.text] }, key);
|
|
61
82
|
}
|
|
62
83
|
return _jsxs(Text, { wrap: "truncate-end", children: ["\u25A1 ", t.text] }, key);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zlas.inspectionlog",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Description of zlas.inspectionlog.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ui5",
|
|
7
|
+
"openui5",
|
|
8
|
+
"sapui5"
|
|
9
|
+
],
|
|
10
|
+
"main": "webapp/index.html",
|
|
11
|
+
"dependencies": {},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@ui5/cli": "^4.0.33",
|
|
14
|
+
"@sap/ux-ui5-tooling": "1"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "fiori run --open \"/test/flpSandbox.html?sap-ui-xx-viewCache=false#zlasinspectionlog-tile\"",
|
|
18
|
+
"start-local": "fiori run --config ./ui5-local.yaml --open \"/test/flpSandbox.html?sap-ui-xx-viewCache=false#zlasinspectionlog-tile\"",
|
|
19
|
+
"build": "ui5 build --config=ui5.yaml --clean-dest --dest dist",
|
|
20
|
+
"deploy": "fiori verify",
|
|
21
|
+
"deploy-config": "fiori add deploy-config",
|
|
22
|
+
"start-noflp": "fiori run --open \"/index.html?sap-ui-xx-viewCache=false\"",
|
|
23
|
+
"start-variants-management": "fiori run --open \"/preview.html?sap-ui-xx-viewCache=false&fiori-tools-rta-mode=true&sap-ui-rta-skip-flex-validation=true#zlasinspectionlog-tile\""
|
|
24
|
+
},
|
|
25
|
+
"sapux": true
|
|
26
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json
|
|
2
|
+
|
|
3
|
+
specVersion: "4.0"
|
|
4
|
+
metadata:
|
|
5
|
+
name: zlas.inspectionlog
|
|
6
|
+
type: application
|
|
7
|
+
framework:
|
|
8
|
+
name: SAPUI5
|
|
9
|
+
version: 1.120.0
|
|
10
|
+
libraries:
|
|
11
|
+
- name: sap.m
|
|
12
|
+
- name: sap.ui.core
|
|
13
|
+
- name: sap.fe.templates
|
|
14
|
+
- name: sap.ushell
|
|
15
|
+
- name: themelib_sap_fiori_3
|
|
16
|
+
server:
|
|
17
|
+
customMiddleware:
|
|
18
|
+
- name: fiori-tools-appreload
|
|
19
|
+
afterMiddleware: compression
|
|
20
|
+
configuration:
|
|
21
|
+
port: 35729
|
|
22
|
+
path: webapp
|
|
23
|
+
delay: 300
|
|
24
|
+
- name: fiori-tools-preview
|
|
25
|
+
afterMiddleware: fiori-tools-appreload
|
|
26
|
+
configuration:
|
|
27
|
+
flp:
|
|
28
|
+
theme: sap_fiori_3
|
|
29
|
+
path: test/flpSandbox.html
|
|
30
|
+
intent:
|
|
31
|
+
object: zlasinspectionlog
|
|
32
|
+
action: tile
|
|
33
|
+
- name: fiori-tools-proxy
|
|
34
|
+
afterMiddleware: compression
|
|
35
|
+
configuration:
|
|
36
|
+
ignoreCertErrors: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted
|
|
37
|
+
backend:
|
|
38
|
+
- path: /sap
|
|
39
|
+
url: https://172.16.24.98:44300
|
|
40
|
+
client: '100'
|
|
41
|
+
builder:
|
|
42
|
+
resources:
|
|
43
|
+
excludes:
|
|
44
|
+
- /test/**
|
|
45
|
+
- /localService/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json
|
|
2
|
+
|
|
3
|
+
specVersion: "4.0"
|
|
4
|
+
metadata:
|
|
5
|
+
name: zlas.inspectionlog
|
|
6
|
+
type: application
|
|
7
|
+
server:
|
|
8
|
+
customMiddleware:
|
|
9
|
+
- name: fiori-tools-proxy
|
|
10
|
+
afterMiddleware: compression
|
|
11
|
+
configuration:
|
|
12
|
+
ignoreCertErrors: true # Dev-only: accept the backend's self-signed certificate
|
|
13
|
+
ui5:
|
|
14
|
+
path:
|
|
15
|
+
- /resources
|
|
16
|
+
- /test-resources
|
|
17
|
+
url: https://ui5.sap.com
|
|
18
|
+
backend:
|
|
19
|
+
- path: /sap
|
|
20
|
+
url: https://172.16.24.98:44300
|
|
21
|
+
client: '100'
|
|
22
|
+
- name: fiori-tools-appreload
|
|
23
|
+
afterMiddleware: compression
|
|
24
|
+
configuration:
|
|
25
|
+
port: 35729
|
|
26
|
+
path: webapp
|
|
27
|
+
delay: 300
|
|
28
|
+
- name: fiori-tools-preview
|
|
29
|
+
afterMiddleware: fiori-tools-appreload
|
|
30
|
+
configuration:
|
|
31
|
+
flp:
|
|
32
|
+
theme: sap_fiori_3
|
|
33
|
+
path: test/flpSandbox.html
|
|
34
|
+
intent:
|
|
35
|
+
object: zlasinspectionlog
|
|
36
|
+
action: tile
|
|
37
|
+
builder:
|
|
38
|
+
resources:
|
|
39
|
+
excludes:
|
|
40
|
+
- /test/**
|
|
41
|
+
- /localService/**
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
7
|
+
<title>Equipment Inspection Logs</title>
|
|
8
|
+
<style>
|
|
9
|
+
html, body, body > div, #container, #container-uiarea {
|
|
10
|
+
height: 100%;
|
|
11
|
+
}
|
|
12
|
+
</style>
|
|
13
|
+
<script
|
|
14
|
+
id="sap-ui-bootstrap"
|
|
15
|
+
src="resources/sap-ui-core.js"
|
|
16
|
+
data-sap-ui-theme="sap_fiori_3"
|
|
17
|
+
data-sap-ui-resource-roots='{
|
|
18
|
+
"zlas.inspectionlog": "./"
|
|
19
|
+
}'
|
|
20
|
+
data-sap-ui-on-init="module:sap/ui/core/ComponentSupport"
|
|
21
|
+
data-sap-ui-compat-version="edge"
|
|
22
|
+
data-sap-ui-async="true"
|
|
23
|
+
data-sap-ui-frame-options="trusted"
|
|
24
|
+
></script>
|
|
25
|
+
</head>
|
|
26
|
+
<body class="sapUiBody sapUiSizeCompact" id="content">
|
|
27
|
+
<div
|
|
28
|
+
data-sap-ui-component
|
|
29
|
+
data-name="zlas.inspectionlog"
|
|
30
|
+
data-id="container"
|
|
31
|
+
data-settings='{"id" : "zlas.inspectionlog"}'
|
|
32
|
+
data-handle-validation="true"
|
|
33
|
+
></div>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|