From 9818b17ab7ecf7822741dd2a91a951c3ba4fd09d Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 07:47:26 +0000 Subject: [PATCH 1/7] fix(motion): apply clamping when inMin equals inMax in mapRange --- packages/motion/src/interpolate.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/motion/src/interpolate.ts b/packages/motion/src/interpolate.ts index 0d1c4866c..c4794bdca 100644 --- a/packages/motion/src/interpolate.ts +++ b/packages/motion/src/interpolate.ts @@ -21,6 +21,9 @@ export function mapRange( const clamp = options?.clamp ?? true; if (inMin === inMax) { + if (clamp && outMin !== outMax) { + return Math.min(outMin, outMax); + } return outMin; } From 8fc986afc3078c1b331114789d3128dfe8b39947 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 07:48:39 +0000 Subject: [PATCH 2/7] fix(core): validate parsed JSON in CommandHistory.import() is an array --- packages/core/src/history/CommandHistory.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/src/history/CommandHistory.ts b/packages/core/src/history/CommandHistory.ts index 20fc4d216..9f420a828 100644 --- a/packages/core/src/history/CommandHistory.ts +++ b/packages/core/src/history/CommandHistory.ts @@ -99,7 +99,15 @@ export class CommandHistory { /** Restore history from a JSON string produced by {@link export}. */ import(data: string): void { - const parsed: string[] = JSON.parse(data); + let parsed: unknown; + try { + parsed = JSON.parse(data); + } catch { + return; + } + if (!Array.isArray(parsed) || !parsed.every(item => typeof item === 'string')) { + return; + } this.commands = parsed.slice(-this.maxSize); this.index = this.commands.length; } From 73a96879214a2479f3ec6fd8169ad51e05388991 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 07:49:27 +0000 Subject: [PATCH 3/7] test(core): add validation tests for CommandHistory.import() --- .../core/src/history/CommandHistory.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/core/src/history/CommandHistory.test.ts b/packages/core/src/history/CommandHistory.test.ts index 65906958a..52b2137c4 100644 --- a/packages/core/src/history/CommandHistory.test.ts +++ b/packages/core/src/history/CommandHistory.test.ts @@ -199,4 +199,29 @@ describe('CommandHistory', () => { expect(ch.getAll()).toHaveLength(3); expect(ch.getAll()).toEqual(smallHistory); }); + + it('import() is a no-op for malformed JSON', () => { + const ch = new CommandHistory(); + ch.add('existing'); + ch.import('not valid json {{{'); + expect(ch.getAll()).toEqual(['existing']); + }); + + it('import() is a no-op for non-array JSON values', () => { + const ch = new CommandHistory(); + ch.add('existing'); + ch.import(JSON.stringify({ commands: ['cmd1', 'cmd2'] })); + expect(ch.getAll()).toEqual(['existing']); + ch.import(JSON.stringify(42)); + expect(ch.getAll()).toEqual(['existing']); + ch.import(JSON.stringify(null)); + expect(ch.getAll()).toEqual(['existing']); + }); + + it('import() is a no-op for arrays containing non-string elements', () => { + const ch = new CommandHistory(); + ch.add('existing'); + ch.import(JSON.stringify(['valid', 123, 'also valid'] as unknown[])); + expect(ch.getAll()).toEqual(['existing']); + }); }); \ No newline at end of file From f65f818f72138e4881c49778aaf31fe7b7529bb8 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 08:00:18 +0000 Subject: [PATCH 4/7] fix(store): construct nested paths from null/empty objects in setIn --- packages/store/src/immutable.ts | 40 ++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/store/src/immutable.ts b/packages/store/src/immutable.ts index c0acdc5ae..d406a7775 100644 --- a/packages/store/src/immutable.ts +++ b/packages/store/src/immutable.ts @@ -24,13 +24,47 @@ export function setIn(obj: T, path: (string | number)[], value: any): T { clone = [...obj]; } else if (obj !== null && typeof obj === 'object') { clone = { ...obj }; + // If clone is effectively empty (from spreading {} or null-ish) and + // the next key is numeric, replace with an array so that clone[0] = x + // creates a real array element instead of a string-keyed property. + if (clone && typeof key === 'number' && Object.keys(clone).length === 0) { + clone = []; + } } else { + // obj is null or a primitive; create the appropriate container + // based on the type of the next key to construct the nested path clone = typeof key === 'number' ? [] : {}; } - const nextVal = rest.length > 0 - ? setIn(clone[key], rest, value) - : value; + // When recursing, determine the correct container for the next level. + // Reuse an existing non-empty object/array to preserve sibling values. + // Otherwise create a container matching the key type: array for numeric keys, + // object for string keys. + const existing = clone[key]; + const isNonEmptyContainer = existing !== undefined + && existing !== null + && typeof existing === 'object' + && Object.keys(existing).length > 0; + + // For the terminal step (rest.length === 0) with a string key and an empty + // container, set the value directly on clone. If clone is an array and the key + // is a string, convert to a plain object first so the property survives + // JSON serialization; for numeric keys the array form is correct. + if (rest.length === 0) { + if (typeof key !== 'number' && Array.isArray(clone)) { + // Convert array to plain object to preserve the string-keyed property + const converted: any = {}; + for (const k of Object.keys(clone)) converted[k] = clone[k as any]; + clone = converted; + } + clone[key] = value; + return clone as T; + } + + const container = isNonEmptyContainer + ? existing + : (typeof key === 'number' ? [] : {}); + const nextVal = setIn(container, rest, value); clone[key] = nextVal; return clone as T; From 7ea2f943ae07bbdc5d7a0b47730556457cccbcd1 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 08:00:42 +0000 Subject: [PATCH 5/7] test(store): add null/empty source cases for setIn --- packages/store/src/immutable.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/store/src/immutable.test.ts b/packages/store/src/immutable.test.ts index 47ed8762e..01afccbae 100644 --- a/packages/store/src/immutable.test.ts +++ b/packages/store/src/immutable.test.ts @@ -43,6 +43,17 @@ describe('immutable helpers', () => { expect(updated).toEqual({ list: [1, 20, 3] }); expect(original.list).toEqual([1, 2, 3]); // should not mutate }); + + it('setIn from null builds nested path correctly', () => { + const updated = setIn(null, ['a', 'b'], 'value'); + expect(updated).toEqual({ a: { b: 'value' } }); + }); + + it('setIn from null with numeric index creates array', () => { + const updated = setIn(null, ['a', 0, 'b'], 'value'); + expect(updated).toEqual({ a: [{ b: 'value' }] }); + expect(Array.isArray((updated as any).a)).toBe(true); + }); }); describe('updateIn', () => { From 9641abc28b0779669a87d7e6d9fc49210fe5e181 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 08:01:45 +0000 Subject: [PATCH 6/7] refactor(ui/quick): use activeIndex in Tabs constructor, remove duplicate active property --- packages/quick/src/widgets.ts | 9 ++++----- packages/ui/src/Tabs.ts | 3 +++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/quick/src/widgets.ts b/packages/quick/src/widgets.ts index 1856e4134..b70e2ba88 100644 --- a/packages/quick/src/widgets.ts +++ b/packages/quick/src/widgets.ts @@ -402,14 +402,13 @@ export function tabs(items: Array<[string, Widget]>, opts?: QuickTabsOptions): W const formattedTabs = items.map(([label, content]) => ({ label, content })); const t = new Tabs(formattedTabs, { activeIndex: opts?.active ?? 0, - active: opts?.active ?? 0 - } as any); // as any: Tabs constructor options not exported from @termuijs/ui + }); if (opts?.onChange) { - if (typeof (t as any).on === 'function') { // as any: Tabs constructor options not exported from @termuijs/ui - (t as any).on('change', opts.onChange); // as any: Tabs constructor options not exported from @termuijs/ui + if (typeof (t as any).on === 'function') { // as any: Tabs does not expose change event API + (t as any).on('change', opts.onChange); // as any: Tabs does not expose change event API } else { - (t as any).onChange = opts.onChange; // as any: Tabs constructor options not exported from @termuijs/ui + (t as any).onChange = opts.onChange; // as any: Tabs does not expose onChange option } } diff --git a/packages/ui/src/Tabs.ts b/packages/ui/src/Tabs.ts index 2185b6366..fff59165a 100644 --- a/packages/ui/src/Tabs.ts +++ b/packages/ui/src/Tabs.ts @@ -7,6 +7,8 @@ export interface TabsOptions { activeColor?: Style['fg']; inactiveColor?: Style['fg']; border?: Style['border']; + /** Initial active tab index. Defaults to 0. */ + activeIndex?: number; } export class Tabs extends Widget { @@ -21,6 +23,7 @@ export class Tabs extends Widget { constructor(tabs: Tab[], options: TabsOptions = {}) { super(mergeStyles(defaultStyle(), { flexGrow: 1, border: options.border ?? 'single' })); this._tabs = tabs; + this._activeIndex = options.activeIndex ?? 0; this._activeColor = options.activeColor ?? { type: 'named', name: 'cyan' }; this._inactiveColor = options.inactiveColor ?? { type: 'named', name: 'brightBlack' }; this.events.on('key', this._keyHandler); From c8a150552a4d083a38f5bc22134a95857093b524 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Mon, 27 Jul 2026 08:02:51 +0000 Subject: [PATCH 7/7] feat(tss): support scientific notation in TSS tokenizer --- packages/tss/src/tokenizer.test.ts | 21 +++++++++++++++++++++ packages/tss/src/tokenizer.ts | 8 +++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/tss/src/tokenizer.test.ts b/packages/tss/src/tokenizer.test.ts index 2adc357ca..f902fbc9d 100644 --- a/packages/tss/src/tokenizer.test.ts +++ b/packages/tss/src/tokenizer.test.ts @@ -46,6 +46,27 @@ describe('TSS Tokenizer', () => { expect(num!.value).toBe('-2'); }); + it('tokenizes scientific notation with positive exponent', () => { + const tokens = tokenize('opacity: 1e5;'); + const num = tokens.find(t => t.type === TokenType.Number); + expect(num).toBeDefined(); + expect(num!.value).toBe('1e5'); + }); + + it('tokenizes scientific notation with negative exponent', () => { + const tokens = tokenize('width: 1.5e-2;'); + const num = tokens.find(t => t.type === TokenType.Number); + expect(num).toBeDefined(); + expect(num!.value).toBe('1.5e-2'); + }); + + it('tokenizes scientific notation with explicit positive sign', () => { + const tokens = tokenize('scale: 2E+3;'); + const num = tokens.find(t => t.type === TokenType.Number); + expect(num).toBeDefined(); + expect(num!.value).toBe('2E+3'); + }); + it('tokenizes CSS variables --name', () => { const tokens = tokenize('--primary: cyan;'); const variable = tokens.find(t => t.type === TokenType.Variable); diff --git a/packages/tss/src/tokenizer.ts b/packages/tss/src/tokenizer.ts index a6809ca75..19237dc52 100644 --- a/packages/tss/src/tokenizer.ts +++ b/packages/tss/src/tokenizer.ts @@ -141,12 +141,18 @@ export function tokenize(source: string): Token[] { continue; } - // Numbers + // Numbers (including scientific notation: 1e5, 1.5e-2, 2E+3) if (/[0-9]/.test(ch) || (ch === '-' && /[0-9.]/.test(at(pos + 1)))) { const startCol = col; let num = ''; if (ch === '-') num += advance(); while (pos < source.length && /[0-9.]/.test(peek())) num += advance(); + // Handle exponent: e or E, optionally followed by +/- then digits + if (pos < source.length && /[eE]/.test(peek())) { + num += advance(); + if (pos < source.length && /[+-]/.test(peek())) num += advance(); + while (pos < source.length && /[0-9]/.test(peek())) num += advance(); + } tokens.push({ type: TokenType.Number, value: num, line, col: startCol }); continue; }